---------------------------------------------------------------------------------------------------
--
-- rover.lua
--
-- The rover activity of the Mars App
--
-- Author: Mike Friebel
-- 
---------------------------------------------------------------------------------------------------
-- Overview
---------------------------------------------------------------------------------------------------
--
-- This activity portrays a rover traversing the Sinai Planum of Mars. It features a physics-based,
-- side-scrolling view and an accompanying overhead view. The side-scrolling movement is generated
-- by directly rotating the rover's wheels and letting Corona's physics engine generate the
-- resulting friction-based displacement, which yields a more realistic motion. The overhead view 
-- depicts the rover's current location on the Sinai Planum and is interactive, allowing the user  
-- to zoom in or out and to generate a new rover course at any time by touching any point of the 
-- overhead image. The overhead view does not automatically follow the rover's movement. It instead
-- remains static until the user zooms in or out, at which point it will pan towards the rover's 
-- current position to the extent allowed by the Sinai Planum image bounds.
--
---------------------------------------------------------------------------------------------------
-- The Terrain
---------------------------------------------------------------------------------------------------
-- 
-- The default side view terrain is generic with randomly generated physics objects to simulate 
-- rocks and is not based on any specific actual terrain. However, the major craters viewable in 
-- the overhead Sinai Planum image are generated as terrain in the side view when approached by 
-- the rover.
--
-- The crater terrain generated is based on a rough estimation of the profiles of actual craters 
-- in reference to their radii. They could be much more accurately modeled by a Chebyshev 
-- polynomial function and this may be something to consider adding in the future. The scale of the
-- craters relative to each other is roughly accurate, however, their scale relative to the rover 
-- is much smaller than actual for the sake of playability. Furthermore, although overall crater 
-- scale has been decreased, crater height has been independently increased for playability while 
-- also being capped to ensure all terrain remains sufficiently within view. This arrangement was 
-- developed in an attempt to adapt realistic crater profiles to the side view's limited dimensions
-- while keeping them recognizable and exciting. Unscaled craters would often exceed the viewing 
-- area while simply decreasing the scale of all craters would make the smaller craters 
-- insignificant.
--
-- One solution to representing larger terrain features in the side view might be to scale the 
-- entire contents of the side view as needed, in a zoom-out effect. This may work for any of the 
-- features in the sedate Sinai Planum but it would likely not work for the even larger features 
-- found elsewhere on Mars. Another solution might be to scroll vertically as well as horizontally 
-- while including an inset that would provide a macroscopic view of the rover's location relative 
-- to the terrain feature being traversed. An indication of elevation could also be provided. Some 
-- combination of these could probably be used.
--
---------------------------------------------------------------------------------------------------
-- Important Terrain Variables
---------------------------------------------------------------------------------------------------
--
-- data.marsToMapScale: actual Mars scale relative to the scale of the overhead view. The current 
-- value is calculated by dividing the estimated actual length of the area depicted in the Sinai 
-- Planum image in meters by the length of the image itself in corona units. This value affects the
-- scale of the crater features generated in the side view. 
--
-- data.mapScaleFactor: scales the terrain features generated in the side view. The scale of the 
-- terrain will be realistic (in accordance with data.marsToMapScale) if this value is 1.
--
-- data.craterHeightScale: scales the height of the crater terrain generated in the side view. 
--
---------------------------------------------------------------------------------------------------
-- Rover Speed
---------------------------------------------------------------------------------------------------
--
-- The speed of the rover in the side view is based on an actual rover length of 5.39 meters. The
-- speed of the rover in the overhead view is much greater than that of the side view relative to
-- the actual dimensions of the area depicted in the Sinai Planum image. This was done for 
-- playability. If the speed relationship was one-to-one, it would take several hours for the rover 
-- to traverse the overhead image. A slower speed would be preferable if the density of interesting
-- terrain features to be encountered in the overhead view and generated in the side view were 
-- greater.
--
---------------------------------------------------------------------------------------------------
-- Important Rover Speed Variables
---------------------------------------------------------------------------------------------------
--
-- data.sideToMarsScale: scale of the side view relative to actual Mars scale. The current value is
-- calculated by dividing the representative length of the side view based on a rover length of 
-- 5.39 meters by the estimated actual length of the area depicted in the Sinai Planum image in 
-- meters. This value affects the speed of the tracking dot in the overhead view.
--
-- data.mapSpeedFactor: scales the speed of the tracking dot across the overhead view. The tracking
-- dot's speed will be realistic (in accordance with data.sideToMarsScale) if this value is 1.
--
---------------------------------------------------------------------------------------------------
-- Overhead View Scaling and Panning
---------------------------------------------------------------------------------------------------
--
-- The overhead map image belongs to data.mapZoomGrp while the course and tracking dot display
-- objects belong to the data.mapGrp container. This allows the map to be scaled and panned without
-- affecting the appearance of the tracking dot or the course. data.mapZoomGrp scales and pans via
-- a transition and the tracking dot is coordinated with data.mapZoomGrp via its own transition.
-- data.mapGrp remains static. When determining spatial relationships, the features of interest
-- must be in reference to the same coordinate system. Coordinates may be converted between the
-- systems by applying or removing data.mapZoomGrp scaling and panning as appropriate. A distance
-- may be converted by applying or removing data.mapZoomGrp scaling as appropriate. data.mapZoomGrp
-- scaling is contained by the data.mapZoomGrp.xScale and data.mapZoomGrp.yScale fields. Their
-- values are always identical as currently implemented. data.mapZoomGrp panning is contained by
-- the data.mapZoomGrp.x and data.mapZoomGrp.y fields, which are also inherently scaled. Touch
-- events are of the data.mapZoomGrp coordinate system. Coordinates may be converted by applying or
-- removing scaling and panning as follows:
--
--         To apply scaling/panning and convert to data.mapZoomGrp:
--
--             newX = oldX * data.mapZoomGrp.xScale + data.mapZoomGrp.x
--
--         To remove map scaling/panning and convert to data.mapGrp:
--
--             newX = (oldX - data.mapZoomGrp.x) / data.mapZoomGrp.xScale
--
---------------------------------------------------------------------------------------------------
-- Known issues
---------------------------------------------------------------------------------------------------
--
-- There is a slight and momentary irregularity in the distance-from-ship calculation during the 
-- simultaneous map scaling and tracking dot movement transitions that occur when the map is both 
-- scaled and panned. It affects return-to-ship functionality when the rover is located 2.000-2.015
-- corona units from the ship and the user zooms to the original map scale, or when the rover is 
-- located 1.985-2.000 corona units from the ship and the user zooms from the original map scale. 
-- This is likely due to the dependency on simultaneous transitions. The easiest fix is probably to
-- avoid performing the calculation during the transitions. 
--
-- There is jitter in the course pointer along its parallel that appeared with changes made in 
-- pull request #187. It is likely due to the calculation in util.calcCourseCoords that ensures 
-- the pointer doesn't exceed the map's boundaries.
--
-- The vertical placement of obstacles on crater terrain is often inconsistent, with some obstacles
-- placed too low and others too high relative to the base terrain's height. This is due to a 
-- temporary hack in new.obstacle() pending the changes needed in util.findTerrainHeight() and 
-- util.findTerrainSlope() to properly handle crater terrain polygons.
--
-- Terrain is incorrectly generated in the side view when a new course is selected while the 
-- rover is located within a crater. The entire crater is generated rather than the fraction 
-- required for display. Only the portion of the crater that lies within rover.x +/- act.width 
-- should be generated. See mapTouched() and newCourseHeightMap() in rover.lua.
--
-- The rover in the side view is too stable and will usually land upright after flipping. This
-- is due to the physics parameters used for the rover body and rover wheels. Stability may be
-- decreased by increasing the density of the body and/or decreasing the density of the wheels.
-- Increasing wheel friction will also decrease stability. The rover also tends to roll too easily
-- over its body when overturned. Decreasing body friction and/or bounce may reduce this. If this 
-- is insufficient, then a procedure to orchestrate a digging-in or sliding stop in the soft 
-- Martian soil when overturned would be necessary. 
--
-- The recover function is currently broken. It sometimes results in a runtime error or the failure 
-- to generate terrain following recovery. This issue appeared following the latest changes made to
-- terrain generation. Check onRecoverPress(), new.Rover(), and moveTerrain().
--
---------------------------------------------------------------------------------------------------
-- Ideas for Additional Features
---------------------------------------------------------------------------------------------------
--
-- Things to see and do while exploring Mars
-- Side view foreground and background elements
-- A greater number of terrain features
-- Larger terrain features
-- Irregular terrain features
-- Pitch-modulated rover sound
-- Additional sound effects (tires, sand, thumps, bangs, etc)
-- Rover track generation on overhead view image
-- Ability for the rover to sustain damage
-- Dust effects generated by the rover's wheels
-- Subversive Martians
-- 
---------------------------------------------------------------------------------------------------

-- Load the Corona widget module
local widget = require "widget" 

-- Get local reference to the game globals
local game = globalGame

-- Create the act object
local act = game.newAct()

-- Load rover data table
local data = require( "roverdata" )

-- Assign reference in roverdata.lua to act object
data.act = act

-- Load rover utility functions and new display object functions
local util = require( "roverutil" )
local new = require( "rovernew" )

------------------------------- Start of Activity -------------------------------

-- Create new crater height map by filling data.craterHeightMap[] with crater height values
-- Requires a data.cratersOnCourse index. Calculations are based on the following approximations: 
-- Peak-to-floor of 4/3r, slope horizontal extent of 0.3r, floor diameter of 0.6r, rim peak of 0.1r
-- (data.marsToMapScale * data.mapScaleFactor) SHOULD BE MOVED TO roverdata.lua TO AVOID RECALCULATION
local function newCraterHeightMap( craterIndex )
	local craterRadius = data.cratersOnCourse[craterIndex].r * data.marsToMapScale * data.mapScaleFactor
	local totalHeight = 4/3 * craterRadius * data.craterHeightScale

	if totalHeight > data.maxCraterHeight then
		totalHeight = data.maxCraterHeight
	end

	local floorHeight = act.height/11 + (data.maxCraterHeight - totalHeight) * data.elevationFactor
	local rimHeight = floorHeight + totalHeight
	local innerSlopeStep = totalHeight / (0.3 * craterRadius)
	local outerSlopeStep = (data.defaultElevation - rimHeight) / (0.3 * craterRadius)

	-- Set floor values
	for d = #data.craterHeightMap + 1, 0.3 * craterRadius do
		data.craterHeightMap[d] = floorHeight
	end

	-- Set inside slope values
	for d = #data.craterHeightMap + 1, 0.6 * craterRadius do
		data.craterHeightMap[d] = data.craterHeightMap[d - 1] + innerSlopeStep
	end

	-- Set inside peak values
	local nPoints = 0.05 * craterRadius
	local peakSlopeStep = innerSlopeStep/nPoints
	local firstPoint = #data.craterHeightMap + 1
	for d = #data.craterHeightMap + 1, 0.65 * craterRadius do
		data.craterHeightMap[d] = data.craterHeightMap[d - 1] + (innerSlopeStep - (d - firstPoint) * peakSlopeStep)
	end

	-- Set outside peak values by copying inside peak values in reverse for a mirror image
	firstPoint = #data.craterHeightMap
	for d = #data.craterHeightMap + 1, 0.7 * craterRadius do
		data.craterHeightMap[d] = data.craterHeightMap[firstPoint + (firstPoint + 1 - d)]
	end

	-- Set outside slope values
	for d = #data.craterHeightMap + 1, craterRadius do
		data.craterHeightMap[d] = data.craterHeightMap[d - 1] + outerSlopeStep
	end
end

-- Fill data.courseHeightMap[] with course-crater intercept height values by indexing data.craterHeightMap
-- with distances from crater center measured along the current course through the crater's extent
local function newCourseHeightMap( craterIndex )
	local craterX = data.cratersOnCourse[craterIndex].x
	local craterY = data.cratersOnCourse[craterIndex].y
	local currentX = data.cratersOnCourse[craterIndex].interceptX
	local currentY = data.cratersOnCourse[craterIndex].interceptY
	local craterR = data.cratersOnCourse[craterIndex].r * data.marsToMapScale * data.mapScaleFactor
	local craterD = math.round(util.calcDistance( currentX, currentY, craterX, craterY ) * data.marsToMapScale * data.mapScaleFactor)

	-- Correct distance-from-crater-center in the case that it is rounded to exceed craterR
	if craterD > craterR then
		craterD = #data.craterHeightMap
	end

	local i = 1
	while craterD <= craterR do

		-- Avoid indexing by 0
		if craterD == 0 then
			craterD = 1
		end

		-- Get height for the current distance from crater center, then get the distance for the next point along the course
		data.courseHeightMap[i] = data.craterHeightMap[craterD]
		currentX = currentX + data.map.courseVX / (data.marsToMapScale * data.mapScaleFactor)
		currentY = currentY + data.map.courseVY / (data.marsToMapScale * data.mapScaleFactor)
		craterD = math.round(util.calcDistance( currentX, currentY, craterX, craterY ) * data.marsToMapScale * data.mapScaleFactor)
		i = i + 1
	end 
end

-- Check the craters on course for crater intercept
local function checkCraters()
	local roverX = data.map.rover.x
	local roverY = data.map.rover.y 
	local cratersToRemove = {}

	for i = 1, #data.cratersOnCourse do

		-- Convert crater coordinates to data.mapZoomGrp, then get crater distance and scaled radius
		local craterX, craterY
		craterX, craterY = util.calcZoomCoords( data.cratersOnCourse[i].x, data.cratersOnCourse[i].y )
		local craterR = data.cratersOnCourse[i].r * data.mapZoomGrp.xScale
		local craterD = util.calcDistance( roverX, roverY, craterX, craterY )

		-- If rover has intercepted a crater: get course height map, flag for drawing, and remove from data.cratersOnCourse
		if craterD <= craterR then 
			util.calcCraterIntercept( i )
			newCraterHeightMap( i )
			newCourseHeightMap( i )	
			data.drawingCrater = true	
			cratersToRemove[#cratersToRemove + 1] = i
		end
	end

	for i = 1, #cratersToRemove do
		table.remove( data.cratersOnCourse, cratersToRemove[i] )
	end
end

-- Map touch event handler
local function mapTouched( event )

	-- If map touch is initiated or moved, then draw a new course
	if event.phase == "began" or event.phase == "moved" then
		local roverX = data.map.rover.x
		local roverY = data.map.rover.y
		local courseX = event.x - data.mapGrp.x 
		local courseY = event.y - data.mapGrp.y

		if courseX ~= roverX or courseY ~= roverY then  -- If course length is non-zero
			util.calcUnitVectors( roverX, roverY, courseX, courseY )
			courseX, courseY = util.calcCourseCoords( data.mapGrp, roverX, roverY, courseX, courseY )

			-- updatePosition() applies scaling/panning to the course coordinates so remove prior to saving
			game.saveState.rover.x2 = (courseX - data.mapZoomGrp.x) / data.mapZoomGrp.xScale
			game.saveState.rover.y2 = (courseY - data.mapZoomGrp.y) / data.mapZoomGrp.yScale
		end
	-- If touch ended, get craters on course, check for intercept, and redraw side view terrain, if necessary
	-- CRATER REDERAW NEEDS WORK! 
	elseif event.phase == "ended" then
		data.nextX = data.rover.x - 100
		util.findCratersOnCourse()
		checkCraters()

		if data.drawingCrater then  -- ADD CRATER REDRAW FUNCTIONALITY

			local x = data.rover.x
			local y = data.rover.y - 20  -- DETERMINE TERRAIN HEIGHT INSTEAD

			-- Replace the rover
			data.rover:removeSelf()
			data.rover = nil

			for i = 1, #data.wheelSprite do
				data.wheelSprite[i]:removeSelf()
				data.wheelSprite[i] = nil
			end

			new.rover( x, y )

			-- Replace the terrain
			for i = 1, #data.terrain do
				if data.terrain[i] then
					display.remove(data.terrain[i])
					data.terrain[i] = nil
				end
			end

			for i = 1, #data.courseHeightMap do
				new.basicTerrain( 2, data.courseHeightMap[i], false, true )
				data.courseHeightIndex = i
			end

			data.courseHeightIndex = data.courseHeightIndex + 1
			data.drawingCrater = false
		end

		-- Record rover position and enable rover
		data.rover.distOldX = data.rover.x
		data.rover.isActive = true
	end

	return true
end

-- Pan and zoom the map according to data.map.scale and the position of the map tracking dot 
local function mapZoom( event )

	-- Convert the tracking dot position into zoom point coordinates 
	local fullZoomX = (data.map.rover.x - data.mapZoomGrp.x) / data.mapZoomGrp.xScale * data.map.scale  
	local fullZoomY = (data.map.rover.y - data.mapZoomGrp.y) / data.mapZoomGrp.yScale * data.map.scale  

	-- Calculate the coordinate value beyond which the map will leave its container's edges
	local zoomBoundary = (data.map.width * data.map.scale - data.map.width)/2

	-- Limit the zoom point coordinates to zoomBoundary
	local zoomX = -game.pinValue( fullZoomX, -zoomBoundary, zoomBoundary )
	local zoomY = -game.pinValue( fullZoomY, -zoomBoundary, zoomBoundary )

	-- Calculate tracking dot's new coordinates, which will be zero unless fullZoomX or fullZoomY exceeds zoomBoundary
	local roverX = fullZoomX + zoomX
	local roverY = fullZoomY + zoomY

	-- Zoom the map to (zoomX, zoomY)
	local zoomData = {
		x = zoomX,
		y = zoomY,
		xScale = data.map.scale,
		yScale = data.map.scale,
		time = 1000,
	}
	transition.to( data.mapZoomGrp, zoomData )

	-- Move the map tracking dot to (roverX, roverY) and re-enable zoom button on completion
	local dotMoveData = {
		x = roverX,
		y = roverY,
		time = 1000,
		onComplete = function() event.target:setEnabled( true ); end 
	}
	transition.moveTo( data.map.rover, dotMoveData )
end

-- Create new map
-- NEED TO BREAK UP INTO MULTIPLE FUNCTIONS AND MOVE TO rovernew.lua
local function newMap()

	-- Create map background
	local mapX = act.xMin + act.height/6 + 5
	local mapY = act.yMin + act.height/6 + 5 
	local mapBgRect = {}
	for i = 1, 5 do
		mapBgRect[i] = display.newRect( data.displayPanelGrp, mapX, mapY, data.mapLength + 6 - i, data.mapLength + 6 - i )
		mapBgRect[i]:setFillColor( 0.5 - i/10, 0.5 - i/10, 0.5 - i/10 )
	end

	-- Create map display container
	data.mapGrp = display.newContainer( data.displayPanelGrp, data.mapLength, data.mapLength )
	data.mapGrp:translate( mapX, mapY )

	-- Initialize rover map starting coordinates to map center
	game.saveState.rover.x1 = 0
	game.saveState.rover.y1 = 0

	data.mapZoomGrp = act:newGroup( data.mapGrp )

	-- Create map
	local mapData = { 
		parent = data.mapZoomGrp, 
		x = 0, 
		y = 0, 
		width = data.mapLength,
		height = data.mapLength
	} 

	data.map = act:newImage( "sinai_planum.png", mapData )
	data.map.scale = 1

	data.mapGrp.left = -data.map.width/2
	data.mapGrp.right = data.map.width/2
	data.mapGrp.top = -data.map.width/2
	data.mapGrp.bottom = data.map.width/2

	-- Add touch event listener to map image
	data.map:addEventListener( "touch", mapTouched )

	-- Create spaceship
	local spaceshipData = { 
		parent = data.mapZoomGrp, 
		x = 0, 
		y = 0, 
		width = 5, 
		height = 5 
	} 
	
	spaceship = act:newImage( "spaceship.png", spaceshipData )	-- UPDATE IMAGE AND DECREASE SIZE

	-- Add tracking dot to the map
	new.mapDot()
	
	-- Add pseudorandomly-generated initial course
	local x1 = data.map.rover.x
	local y1 = data.map.rover.y
	local x2 = x1
	local y2 = y1

	while x2 == x1 and y2 == y1 do
		x2 = math.random(-data.map.width/2, data.map.width/2)
		y2 = math.random(-data.map.width/2, data.map.width/2)
	end

	util.calcUnitVectors( x1, y1, x2, y2 )
	x2, y2 = util.calcCourseCoords( data.mapGrp, x1, y1, x2, y2 )
	game.saveState.rover.x2 = x2
	game.saveState.rover.y2 = y2

	util.findCratersOnCourse()

	data.map.course = util.newCourse( data.mapGrp, x1, y1, x2, y2 )
	data.map.courseArrow = util.newArrow( act, data.mapGrp, x1, y1, x2 - 2 * data.map.courseVX, y2 - 2 * data.map.courseVY )
	data.map.rover:toFront()	

	-- Record the current x-axis position of the side scrolling rover
	data.rover.distOldX = data.rover.x
end

-- Create new battery indicator. Accepts coordinate pair.
-- MOVE TO rovernew.lua
local function newBattIndicator( x, y )

	-- Create new battery indicator display object
	local batteryData = { 
		parent = data.displayPanelGrp, 
		x = x + 5,
		y = y + 27, 
		width = 26, 
		height = 13, 
	} 

	local battery = act:newImage( "battery.png", batteryData )
	battery.anchorX = 1
	battery.anchorY = 1

	-- Create battery indicator display text object
	local format = "%3d%s"
	local options = 
	{
		parent = data.displayPanelGrp,
		text = string.format( format, game.energy(), "%" ),
		x = x + 2,
		y = y + 25.5,
		font = native.systemFontBold,
		fontSize = 8,
	}

	data.energyText = display.newText( options )
	data.energyText:setFillColor( 0.0, 1.0, 0.0 )
	data.energyText.anchorX = 1
	data.energyText.anchorY = 1
	data.energyText.format = format
end

-- Create new energy gauge
-- BREAK UP AND MOVE TO rovernew.lua
local function newEnergyGauge( x, y )

	-- Options table for energy gauge image sheet 
	local options = {
		width = 60,
		height = 2,
		numFrames = 8,
		sheetContentWidth = 60,
		sheetContentHeight = 16,
	}

	-- Energy gauge variables
	local length = math.round( data.mapLength ) - 16
	local nElements = 50
	local hScale = 1.0
	local height = options.height
	local spacing = 1.25

	-- Create an energy gauge background with attempt to create some shading for apparent recessed depth
	local energyGaugeBg = {}
	for i = 1, length + 5 do
		for j = 1, 6 do
			energyGaugeBg[i] = {}
			-- This adapts the function that produces the gauge's foreground curve to the background (see gauge sprite creation below)
			local width = options.width * math.pow( 2, (i*(nElements/(length + 5))*i*(nElements/(length + 5))/3000)) - (33 - i/100) - j
			energyGaugeBg[i][j] = display.newRect( data.displayPanelGrp, act.xCenter, act.yCenter, width, 1 )
			energyGaugeBg[i][j].anchorX = 1
			energyGaugeBg[i][j].anchorY = 1
			energyGaugeBg[i][j].x = x + 5 - (j - 1)/2
			energyGaugeBg[i][j].y = y - (i - 1)
			energyGaugeBg[i][j]:setFillColor( 0.6 - j/10, 0.6 - j/10, 0.6 - j/10 )
		end
	end

	-- Create top background border with attempt to create some shading for apparent recessed depth
	local energyGaugeBgTopBorder = {}
	for i = 1, 6 do
		energyGaugeBgTopBorder[i] = {}
		-- This fits each background element to the top gauge element width using the function the produces the gauge curve.
		local width = options.width * math.pow( 2, (50-(i-1)/2*50/(length + 5))*(50-(i-1)/2*50/(length + 5))/3000 ) - 33 - i
		energyGaugeBgTopBorder[i] = display.newRect( data.displayPanelGrp, act.xCenter, act.yCenter, width, 1 )
		energyGaugeBgTopBorder[i].anchorX = 1
		energyGaugeBgTopBorder[i].anchorY = 1
		energyGaugeBgTopBorder[i].x = x + 5 - (i - 1)/2
		energyGaugeBgTopBorder[i].y = y - (length + 4) + (i - 1)/2
		energyGaugeBgTopBorder[i]:setFillColor( 0.4 - i/15, 0.4 - i/15, 0.4 - i/15 )
	end

	-- Create bottom background border with attempt to create some shading for apparent recessed depth
	local energyGaugeBgBtmBorder = {}
	for i = 1, 6 do
		energyGaugeBgBtmBorder[i] = {}
		-- This fits each background element to the bottom gauge element width using the function the produces the gauge curve.
		local width = options.width * math.pow( 2, (1*(nElements/(length + 5))*1*(nElements/(length + 5))/3000)) - 33 - i
		energyGaugeBgBtmBorder[i] = display.newRect( data.displayPanelGrp, act.xCenter, act.yCenter, width, 1 )
		energyGaugeBgBtmBorder[i].anchorX = 1
		energyGaugeBgBtmBorder[i].anchorY = 1
		energyGaugeBgBtmBorder[i].x = x + 5 - (i - 1)/2
		energyGaugeBgBtmBorder[i].y = y - (i - 1)/2 + 1
		energyGaugeBgBtmBorder[i]:setFillColor( 0.4 - i/15, 0.4 - i/15, 0.4 - i/15 )
	end

	local gaugeSheet = graphics.newImageSheet( 'media/rover/gauge_sheet.png', options )

	local sequenceData = {
		name = "energySequence",
		start = 1,
		count = 8,
	}

	-- Create energy gauge sprites
	for i = 1, nElements do
		data.energyGaugeSprite[i] = display.newSprite( data.displayPanelGrp, gaugeSheet, sequenceData )
		data.energyGaugeSprite[i].anchorX = 1
		data.energyGaugeSprite[i].anchorY = 1
		data.energyGaugeSprite[i].x = x + 1
		data.energyGaugeSprite[i].y = y - (height * hScale + spacing) * (i - 1) - 1.51
		-- This is a hack to produce the gauge curve. The exponential function produces the curve while '0.7' adjusts element width
		data.energyGaugeSprite[i]:scale( math.pow( 2, i*i/3000 ) - 0.7, hScale )
		data.energyGaugeSprite[i]:setFrame( 5 )
		data.energyGaugeSprite[i].bright = true
	end
end

-- Zoom-in button handler
local function onZoomInRelease( event )
	if data.map.scale < 3 then
		data.map.scale = data.map.scale * 1.5
		data.zoomInButton:setEnabled( false )
		mapZoom( event )
	end
end

-- Zoom-out button handler
local function onZoomOutRelease( event )
	if data.map.scale > 1 then
		data.map.scale = data.map.scale / 1.5
		data.zoomOutButton:setEnabled( false )
		mapZoom( event )
	end
end

-- Create the display panel
-- BREAK UP AND MOVE TO rovernew.lua
local function newDisplayPanel()

	-- Create display panel background image
	local dispPanelData = { 
		parent = data.displayPanelGrp, 
		x = act.xCenter, 
		y = act.yMin + act.height/6 + 5, 
		width = act.width, 
		height = act.height/3 + 10
	} 

	displayPanel = act:newImage( "panel.png", dispPanelData )

	newMap()

	-- Create zoom-in button
	data.zoomInButton = widget.newButton
	{
	    id = "data.zoomInButton",
	    left = act.xMin + data.map.width + 10, 
	    top = data.mapGrp.y - 20, 
	    width = 40, 
	    height = 40,
	    defaultFile = "media/rover/zoom_in_unpressed.png",
		overFile = "media/rover/zoom_in_pressed.png",
	    onRelease = onZoomInRelease
	}

	-- Create zoom-out button
	data.zoomOutButton = widget.newButton
	{
	    id = "data.zoomOutButton",
	    left = act.xMin + data.map.width + 10, 
	    top = data.mapGrp.y + 20, 
	    width = 40, 
	    height = 40,
	    defaultFile = "media/rover/zoom_out_unpressed.png",
		overFile = "media/rover/zoom_out_pressed.png",
	    onRelease = onZoomOutRelease
	}

	-- Create background border for the speed display
	speedBgRect = {}
	for i = 1, 5 do
		speedBgRect[i] = display.newRect( data.displayPanelGrp, act.xCenter, act.yCenter, 86 - i, 26 - i )
		speedBgRect[i].x = act.xMin + data.map.width + 53
		speedBgRect[i].y = act.yMin + data.map.width - 5
		speedBgRect[i]:setFillColor( 0.5 - i/10, 0.5 - i/10, 0.5 - i/10 )
	end

	speedBgRect[6] = display.newRect( data.displayPanelGrp, act.xCenter, act.yCenter, 80, 20 )
	speedBgRect[6].x = act.xMin + data.map.width + 53
	speedBgRect[6].y = act.yMin + data.map.width - 5
	speedBgRect[6]:setFillColor( 0.2, 0.2, 0.2 )

	-- Create speed display text object
	local format = "%4d %s"
	local options = 
	{
		parent = data.displayPanelGrp,
		text = string.format( format, 0, "kph" ),
		x = act.xMax - 10,
		y = 20,
		font = native.systemFontBold,
		fontSize = 18,
	}

	data.speedText = display.newText( options )
	data.speedText:setFillColor( 0.0, 1.0, 0.0 )
	data.speedText.anchorX = 1
	data.speedText.anchorY = 1
	data.speedText.x = speedBgRect[6].x + 37
	data.speedText.y = speedBgRect[6].y + 10
	data.speedText.format = format

	data.staticFgGrp:insert( data.zoomInButton )
	data.staticFgGrp:insert( data.zoomOutButton )

	-- Create battery indicator and energy gauge display
	newEnergyGauge( act.xMax - 6, speedBgRect[1].y + speedBgRect[1].height/2 - 16  )
	newBattIndicator( act.xMax - 6, speedBgRect[1].y + speedBgRect[1].height/2 - 27 )
end

-- Set energy gauge color based on energy quintile
local function setEnergyGaugeColor()
	local spriteFrame
	if game.energy() > 80 then
		spriteFrame = 5
	elseif game.energy() > 60 then
		spriteFrame = 4
	elseif game.energy() > 40 then
		spriteFrame = 3
	elseif game.energy() > 20 then
		spriteFrame = 2
	else
		spriteFrame = 1
	end

	for i = 1, data.energyGaugeIndex do
		if data.energyGaugeSprite[i].frame ~= spriteFrame then
			data.energyGaugeSprite[i]:setFrame( spriteFrame )
		end
	end
end

-- Reset energy gauge IAW current energy level
local function resetEnergyGauge()
	setEnergyGaugeColor()

	for i = 1, data.energyGaugeIndex do
		data.energyGaugeSprite[i]:setFillColor( 1 )
		data.energyGaugeSprite[i].isVisible = true
	end

	for i = data.energyGaugeIndex + 1, 50 do
		data.energyGaugeSprite[i].isVisible = false
		data.energyGaugeSprite[i]:setFillColor( 1 )
	end
end

-- Update the battery indicator and energy gauge
local function updateEnergyDisplay()
	-- Update energy text
	data.energyText.text = string.format( data.energyText.format, game.energy() , "%")

	-- If the truncated game.energy() integer has diminished from odd to even then dim the top gauge element
	if math.floor( game.energy() ) % 2 == 0 then
		if data.energyGaugeIndex - math.floor( game.energy() )/2 == 1 then
			if data.energyGaugeSprite[data.energyGaugeIndex].bright then
				data.energyGaugeSprite[data.energyGaugeIndex]:setFillColor( 0.5 )
				data.energyGaugeSprite[data.energyGaugeIndex].bright = false
			end
		else -- Reset the energy gauge because game.energy() has changed in an unpredictable manner
			data.energyGaugeIndex = math.floor( game.energy() )/2
			setEnergyGaugeColor()
			resetEnergyGauge()
		end
	else -- If game.energy() has diminished from even to odd, set top gauge element visibility & color, update index
		if data.energyGaugeIndex - math.ceil( game.energy()/2 ) == 1 then
			data.energyGaugeSprite[data.energyGaugeIndex].isVisible = false
			data.energyGaugeIndex = math.ceil( game.energy()/2 ) -- Could simply decrement the index?
			if data.energyGaugeIndex % 10 == 0 then
				setEnergyGaugeColor()
			end
		-- Reset the energy gauge because game.energy() has changed in an unpredictable manner
		elseif data.energyGaugeIndex - math.ceil( game.energy()/2 ) ~= 0 then
			data.energyGaugeIndex = math.ceil( game.energy()/2 ) 
			setEnergyGaugeColor()
			resetEnergyGauge()
		end
	end
end

-- Accelerate the rover up to an angular velocity of 8000 with higher initial acceleration
local function accelRover()
	if data.rover.angularV <= 150 then
		data.rover.angularV = data.rover.angularV + 50 
	elseif data.rover.isAutoNav and data.rover.angularV + 20 > 2000 then
		data.rover.angularV = 2000
	elseif data.rover.angularV + 20 > 8000 then
		data.rover.angularV = 8000 -- top speed
	else
		data.rover.angularV = data.rover.angularV + 20 -- typical acceleration
	end

	-- Apply energy use
	game.addEnergy( -0.001 )
	updateEnergyDisplay()
end

-- Decelerate the rover; deceleration varies inversely with speed for stability
local function brakeRover()
	if data.rover.kph < 20 then
		data.rover.angularV = 0
	else 
		data.rover.angularV = data.rover.angularV * data.rover.kph/400
	end
end

-- Let the rover coast, with increased deceleration during high angle-of-attack instances for stability
local function coastRover()
	if (data.rover.rotation % 360 > 260 and data.rover.rotation % 360 < 300) then -- If high AOA
		data.wheelSprite[1].linearDampening = 1000
		if data.rover.kph < 10 then
			data.rover.angularV = 0
		else
			data.rover.angularV = data.rover.angularV * 0.9 
		end
	elseif data.rover.angularV > 100 then
		data.rover.angularV = data.rover.angularV * 0.99 -- Normal deceleration
	elseif data.rover.angularV - 1 > 0 then
		data.rover.angularV = data.rover.angularV - 1 -- Final deceleration to 0
	else
		data.rover.angularV = 0
	end
end

-- Decelerate rover
local function decelerate()

	data.ctrlPanelGrp.accelButton:setFrame( 1 ) 
	data.rover.accelerate = false

	-- Play rover deceleration sound followed by idle engine sound, first halting any other sounds
	game.stopSound( data.rover.stage2Channel )
	game.stopSound( data.rover.stage1Channel )
	game.stopSound( data.rover.startChannel )

	local options2 = {
		channel = 1,
		loops = -1,
	}

	local function playEngineSound()
		if ( not data.rover.accelerate and game.currentActName() == "rover" ) then 
			data.rover.engineChannel = game.playSound(data.rover.engineSound, options2); 
		end
	end

	local options1 = {
		channel = 5,
		loops = 0,
		-- Play the rover engine sound indefinitely upon completion
		onComplete = playEngineSound
	}

	data.rover.stopChannel = game.playSound(data.rover.stopSound, options1)
end

-- Update the rover's map position with the scaled distance the rover moved in the scrolling view
local function updateCoordinates()
	local distMoved = ( data.rover.x - data.rover.distOldX ) * data.sideToMarsScale * data.mapSpeedFactor
	data.rover.distOldX = data.rover.x
	data.map.rover.x = data.map.rover.x + (distMoved * data.map.courseVX) * data.mapZoomGrp.xScale
	data.map.rover.y = data.map.rover.y + (distMoved * data.map.courseVY) * data.mapZoomGrp.yScale
end

-- Return to mainAct if the rover has returned to the ship
local function checkIfRoverAtShip()

	local x1 = data.mapZoomGrp.x
	local y1 = data.mapZoomGrp.y
	local x2 = data.map.rover.x
	local y2 = data.map.rover.y

	-- Calculate the distance from the rover (in mapGrp) to the ship (at mapZoomGrp origin)
	local distanceFromShip = util.calcDistance( x1, y1, x2, y2 )

	-- If the rover has returned to the ship, then go to mainAct
	if distanceFromShip <= 2 * data.mapZoomGrp.xScale then
		if not data.rover.atShip then
			data.rover.atShip = true
			game.gotoAct( "mainAct" )
		end
	elseif data.rover.atShip then
		data.rover.atShip = false	
	end
end

-- Engage auto navigation back to the ship (mandatory course, governed speed, hidden water scan button)
-- NEED TO ADD UNZOOMING OF MAP
local function engageAutoNav()
	game.saveState.rover.x2 = 0
	game.saveState.rover.y2 = 0
	data.rover.isAutoNav = true
	data.ctrlPanelGrp.waterButton.isVisible = false

	-- Remove map touch listener to prevent course changes
	data.map:removeEventListener( "touch", mapTouched )

	-- Display message to user 
	local options = { 
		x = act.xMax - 26,
		y = act.yMin + 12,
		time = 3000,
		width = 220
	}

	game.messageBox( "ON RESERVE POWER!\n\nAuto navigation engaged.", options )
end

-- Update the rover's position on the overhead view
-- SHOULD BREAK UP
local function updatePosition()
	if data.rover.isActive then 

		updateCoordinates()
		checkIfRoverAtShip() -- NEED TO NOT DO THIS DURING THE ZOOMING TRANSITIONS
		checkCraters()

		local roverX = data.map.rover.x 
		local roverY = data.map.rover.y 
		local courseX = game.saveState.rover.x2 * data.mapZoomGrp.xScale + data.mapZoomGrp.x
		local courseY = game.saveState.rover.y2 * data.mapZoomGrp.yScale + data.mapZoomGrp.y

		-- If autonav not engaged, then calculate course coords in case of map panning & engage autonav if needed
		if not data.rover.isAutoNav then -- CONSIDER USING A DIFFERENT VARIABLE NAME
			courseX, courseY = util.calcCourseCoords( data.mapGrp, roverX, roverY, courseX, courseY ) -- MAKE THIS ONLY RUN DURING PANNING
			if ( game.energy() <= 0 or game.food() <= 0 ) then
				engageAutoNav()
			end
		end

		-- If the rover is within the map's boundaries, replace the course, else deactivate the rover
		if game.xyInRect( roverX, roverY, data.mapGrp ) and data.map.courseLength > 0 then	-- ARE BOTH CONDITIONS NECESSARY?
			util.replaceCourse( act, data.mapGrp, roverX, roverY, courseX, courseY )	
		else
			-- Deactivate rover, cease acceleration, and initiate braking
			data.rover.isActive = false
			data.rover.accelerate = false
			data.rover.brake = true

			-- Remove map course objects
			display.remove( data.map.course )
			display.remove( data.map.courseArrow )
			data.map.courseLength = 0

			if not audio.isChannelPlaying( 5 ) then		
				decelerate()
			end

			-- Ensure the tracking dot remains within map boundaries
			if math.abs(data.map.rover.x) > data.map.width/2 then
				data.map.rover.x = math.abs(data.map.rover.x) / data.map.rover.x * data.map.width/2 * 0.99  -- USE UNIT VECTORS HERE INSTEAD
			end

			if math.abs(data.map.rover.y) > data.map.width/2 then
				data.map.rover.y = math.abs(data.map.rover.y) / data.map.rover.y * data.map.width/2 * 0.99  -- USE UNIT VECTORS HERE INSTEAD
			end
		end		
	end
end

-- Adjust and apply rover wheel angular velocity
-- BREAK UP INTO MULTIPLE FUNCTIONS
local function moveRover()

	-- Accelerate, brake, or coast rover
	if data.rover.accelerate then
		accelRover()
		data.ctrlPanelGrp.waterButton.isVisible = false
	elseif data.rover.brake then
		brakeRover()
	else
		coastRover()
	end

	-- Apply wheel angular velocity to the wheel sprites with the rear wheel at half speed for stability
	data.wheelSprite[1].angularVelocity = data.rover.angularV/2
	for i = 2, 3 do
		data.wheelSprite[i].angularVelocity = data.rover.angularV
	end

	updatePosition()

	-- Determine and set wheel sprite frame
	local wheelFrame
	if data.rover.angularV > 700 then 
		wheelFrame = 7
	elseif data.rover.angularV < 200 then 
		wheelFrame = 1
	else
		wheelFrame = math.floor( data.rover.angularV/100 )
	end

	for i = 1, 3 do
		data.wheelSprite[i]:setFrame( wheelFrame )
	end

	-- If the rover has stopped overturned, then replace the accelerate button with the recover button after some delay
	-- Otherwise, if the rover has stopped upright, then display the water scan button
	if (data.rover.rotation % 360 > 80 and data.rover.rotation % 360 < 270) and data.rover.kph == 0 then

		data.ctrlPanelGrp.waterButton.isVisible = false

		local function displayRecoverButton( event )
			if (data.rover.rotation % 360 > 80 and data.rover.rotation % 360 < 270) and data.rover.kph == 0 then
				data.ctrlPanelGrp.recoverButton.isVisible = true
				data.ctrlPanelGrp.accelButton.isVisible = false
				data.ctrlPanelGrp.waterButton.isVisible = false
				data.rover.accelerate = false
			end
		end

		timer.performWithDelay( 2000, displayRecoverButton )
	elseif data.rover.kph == 0 then
		data.ctrlPanelGrp.waterButton.isVisible = true
	end
end

-- Generate basic or crater terrain if new terrain is needed
local function moveTerrain()
	if data.terrain[#data.terrain].x + data.terrain[#data.terrain].width <= data.rover.x + act.width then
		if data.drawingCrater then
			new.craterTerrain( false, true )
		else
			new.basicTerrain( data.basicTerrainObjWidth, data.defaultElevation, false, false )
		end
	end
end

-- Acceleration button touch event handler
local function handleAccelButton( event )

	if data.rover.isActive then	

		-- If accelerator touch began, then set accelerator image, set flag for acceleration, & play appropriate sound
		if ( event.phase == "began" ) then

			data.ctrlPanelGrp.accelButton:setFrame( 2 ) 
			data.rover.accelerate = true
			game.stopSound( data.rover.engineChannel )
			game.stopSound( data.rover.stopChannel )

			local options3 = {
				channel = 4,
				loops = -1,
			}

			-- Play stage2 sound indefinitely if rover is accelerating
			local function playStage2Sound()
				if data.rover.accelerate then 
					data.rover.stage2Channel = game.playSound(data.rover.stage2Sound, options3); 
				end
			end

			local options2 = {
				channel = 3,
				loops = 0,
				onComplete = playStage2Sound
			}

			-- Play stage1 sound indefinitely if rover is accelerating
			local function playStage1Sound()
				if data.rover.accelerate then 
					data.rover.stage1Channel = game.playSound(data.rover.stage1Sound, options2); 
				end
			end

			local options1 = {
				channel = 2,
				loops = 0,
				onComplete = playStage1Sound
			}

			-- Play start sound, then stage1 sound, then stage2 sound, depending on wheel angular velocity
			if data.rover.angularV > 3500 then
				data.rover.stage2Channel = game.playSound(data.rover.stage2Sound, options3)
			elseif data.rover.angularV > 1750 then
				data.rover.stage1Channel = game.playSound(data.rover.stage1Sound, options2)
			else
				data.rover.startChannel = game.playSound(data.rover.startSound, options1)
			end

		elseif ( (event.phase == "ended" or event.phase == "cancelled") and data.rover.accelerate == true ) then
			decelerate()
		end 
	end

	return true
end

-- Brake button event handler
local function onBrakePress( event )
	data.rover.accelerate = false
	data.rover.brake = true
	return true
end

-- Brake button event handler
local function onBrakeRelease( event )
	data.rover.brake = false
	return true
end

-- Water scan button event handler: initiate braking, stop all audio, then go to drillScan act.
local function onWaterRelease( event )
	data.rover.brake = true
	audio.stop()
	game.gotoAct ( "drillScan", { effect = "zoomInOutFade", time = 1000 } )
	return true
end

-- Reset button event handler
local function onRecoverPress( event )

	local x = data.rover.x
	local y = data.rover.y

	-- Replace side view rover
	data.rover:removeSelf()
	data.rover = nil
	for i = 1, #data.wheelSprite do
		data.wheelSprite[i]:removeSelf()
		data.wheelSprite[i] = nil
	end
	new.rover( x, y )

	-- Reset speed display
	data.rover.speedOldX = data.rover.x
	data.speedText.text = string.format( data.speedText.format, 0, "kph" )

	-- Replace recover button with accelerator button
	data.ctrlPanelGrp.accelButton.isVisible = true
	data.ctrlPanelGrp.waterButton.isVisible = true
	data.ctrlPanelGrp.recoverButton.isVisible = false

	-- Deduct energy cost
	game.addEnergy( -5.0 )
	updateEnergyDisplay()

	return true
end

-- Handle accelerator button slide-off
local function handleSlideOff( event )
	if ( event.phase == "moved" and data.rover.accelerate == true ) then
		decelerate()
	end
	return true
end

-- Create control panel. BREAK UP AND MOVE TO rovernew.lua
local function newControlPanel()

	-- Set panel backgroud image
	local ctrlPanelData = { 
		parent = data.ctrlPanelGrp, 
		x = act.xCenter, 
		y = act.yMax + act.height/12, 
		width = act.width, 
		height = act.height/3,
	} 
	displayPanel = act:newImage( "panel.png", ctrlPanelData )

	-- Create invisible circle object as slide-off sensor for the accelerator button
	local slideOffSensor = display.newCircle( data.ctrlPanelGrp, act.xCenter + 35, act.yMax - 24, 60 )
	slideOffSensor.isVisible = false
	slideOffSensor.isHitTestable = true
	slideOffSensor:addEventListener( "touch", handleSlideOff )

	-- Create the accelerator button sprite
	local options = {
		width = 128,
		height = 128,
		numFrames = 2
	}
	local accelButtonSheet = graphics.newImageSheet( 'media/rover/accel_button.png', options )

	local sequenceData = {
		name = "accelButtoonSequence",
		start = 1,
		count = 2,
	}
	data.ctrlPanelGrp.accelButton = display.newSprite( data.ctrlPanelGrp, accelButtonSheet, sequenceData )
	data.ctrlPanelGrp.accelButton.x = act.xCenter + 35
	data.ctrlPanelGrp.accelButton.y = act.yMax - 22
	data.ctrlPanelGrp.accelButton:scale( act.height/1707, act.height/1707 )
	data.ctrlPanelGrp.accelButton:addEventListener( "touch", handleAccelButton )

	-- Create the stop button
	local brakeButton = widget.newButton
	{
		x = act.xCenter - 35,
		y = act.yMax - 22,
		width = act.height/13.33,
		height = act.height/13.33,
		defaultFile = "media/rover/brake_unpressed.png",
		overFile = "media/rover/brake_pressed.png",
		onPress = onBrakePress,
		onRelease = onBrakeRelease
	}

	-- Create the water scan button
	data.ctrlPanelGrp.waterButton = widget.newButton
	{
		x = act.xMax - 28,
		y = act.yMax - 22,
		width = act.height/13.33,
		height = act.height/13.33,
		defaultFile = "media/rover/water_unpressed.png",
		overFile = "media/rover/water_pressed.png",
		onRelease = onWaterRelease
	}

	-- Create the reset button
	data.ctrlPanelGrp.recoverButton = widget.newButton
	{
		x = act.xCenter + 35,
		y = act.yMax - 21.5,
		width = act.height/12,
		height = act.height/12,
		defaultFile = "media/rover/reset_unpressed.png",
		overFile = "media/rover/reset_pressed.png",
		onPress = onRecoverPress
	}
	data.ctrlPanelGrp.recoverButton.isVisible = false

	data.ctrlPanelGrp:insert( brakeButton )
	data.ctrlPanelGrp:insert( data.ctrlPanelGrp.waterButton )
	data.ctrlPanelGrp:insert( data.ctrlPanelGrp.recoverButton )
end

-- Update the speed display
local function updateSpeedDisplay()
	local kmPerCoronaUnit = 0.00006838462 -- based on estimated rover length of 4.66 meters
	local elapsedTimePerHr = 7200 -- every 0.5 seconds
	data.rover.kph = ( data.rover.x - data.rover.speedOldX ) * kmPerCoronaUnit * elapsedTimePerHr
		
	if data.rover.kph < 0 then 
		data.rover.kph = 0 
	end

	data.speedText.text = string.format( data.speedText.format, data.rover.kph, "kph" )
	data.rover.speedOldX = data.rover.x
end

-- Start the act
function act:start()
	game.stopAmbientSound()
	data.rover.engineChannel = game.playSound(data.rover.engineSound, { channel = 1, loops = -1 } )
	physics.start()
end	

-- Stop the act
function act:stop()
	game.saveState.rover.x1 = data.map.rover.x
	game.saveState.rover.y1 = data.map.rover.y
	audio.stop()	-- Stop all audio
	physics.pause()
end

-- Initiate the act
function act:init()
	local physics = require "physics"
	physics.start()
	physics.setGravity( 0, 3.3 )
	-- physics.setDrawMode( "hybrid" )
	math.randomseed( os.time() )

	-- Fill data.shape table with terrain obstacle shape functions
	data.shape = { new.circle, new.square, new.roundSquare, new.polygon }

	-- Create display groups
	data.staticBgGrp = act:newGroup()
	data.staticFgGrp = act:newGroup()
	data.dynamicGrp = act:newGroup()	
	data.ctrlPanelGrp = act:newGroup( data.staticFgGrp )
	data.displayPanelGrp = act:newGroup( data.staticFgGrp )

	-- Initialize act-dependent variables
	data.roverPosition = act.xMin + 100
	data.scrollViewTop = act.yMin + act.height/3
	data.scrollViewBtm = act.yMax - act.height/11
	data.defaultElevation = act.height/11 + (data.scrollViewBtm - data.scrollViewTop) * data.elevationFactor
	data.maxCraterHeight = data.scrollViewBtm - data.scrollViewTop - 75
	data.mapLength = act.height/3
	data.energyGaugeIndex = math.ceil( game.energy()/2 )

	-- Create rover, background, object removal sensor, display panel, and control panel
	new.rover( data.roverPosition, act.yMax - data.defaultElevation - 14 )
	new.background()
	new.RemovalSensor()
	newDisplayPanel()
	newControlPanel()
	resetEnergyGauge()
	updateEnergyDisplay()

	-- Create initial terrain
	while data.nextX < data.rover.x + act.width do
		new.basicTerrain( data.basicTerrainObjWidth, data.defaultElevation, false, false )
	end

	for i = 1, data.nObstacles do
		new.obstacle( math.random( data.removalSensor.x, data.rover.x + data.act.width ) )
	end

	-- Start rover speed display updating
	timer.performWithDelay( 500, updateSpeedDisplay, 0 )
end

-- Handle enterFrame events
function act:enterFrame( event )

	-- Set and apply rover wheel angular velocity
	moveRover() 

	-- Move data.dynamicGrp and removal sensor along the x-axis the distance the rover has moved
	data.dynamicGrp.x = data.roverPosition - data.rover.x
	data.removalSensor.x = data.rover.x - act.width

	-- Remove and generate terrain
	moveTerrain()

	-- Set static group stack order
	data.staticBgGrp:toBack()
	data.staticFgGrp:toFront()
end

-------------------------------- End of Activity --------------------------------

-- Corona needs the scene object returned from the act file
return act.scene