-- enable old audio system
System.oaenable();

-- FUNCTIONS

-- Simple delay function, where timer is overkill
function delay(count)
	for i=1,count do
		screen.waitVblankStart()
	end
end

function createBumper(bumper_type,x,y)

	-- types:
	---- 0 (red 100 pts)
	---- 1 (blue 10 pts)
	---- 2 (green targeting scope)
	---- 3 (yellow score bonus)
	---- "fixed"
	---- "active"
	
	-- bumper{type,radius,x,y,hit}
	
	bumper = {}
	bumper.type = bumper_type
	
	if bumper_type == "fixed" or bumper_type == "active" then
		bumper.radius = 16
	else
		bumper.radius = 8
	end
	
	bumper.x = x or math.random(bumper.radius+board.minx+mingap,board.maxx-bumper.radius-mingap) 
	bumper.y = y or math.random(bumper.radius+board.minby,board.maxby-bumper.radius)
	
	bumper.hit=0

	return bumper;
end


function addRandomBumpers(bumper_type, count)
	for i=1,count do
		new_bumper = createBumper(bumper_type)
		
		-- add check for position validity here
		valid = false;
		while not valid do
		
			valid = true;
			for b=1,#bumpers do
				-- check distance to all existing bumpers
				distance = new_bumper.radius + bumpers[b].radius + 13;
				actual_dist = math.sqrt( (new_bumper.x - bumpers[b].x)^2 + (new_bumper.y - bumpers[b].y)^2 );
				if actual_dist < distance then
					valid = false;
					new_bumper = createBumper(bumper_type);
					break;
				end
			end
		
		end
		
		table.insert(bumpers,new_bumper)
	end
end


function setBall(x,y, vx,vy, time)

	-- ball{radius,orig_x,orig_y,orig_vx,orig_vy,x,y,vx,vy,visible}
	
	-- x,y   - current position
	-- vx,vy - current velocity
	-- time  - time at point of origin
	-- orig  - position and velocity at point of origin
	
	if ball == nil then
		ball = {}
		ball.radius = 6
		ball.x = 0
		ball.y = 0
		ball.vx = 0
		ball.vy = 0
		ball.time = 0
	end
	
	ball.x = x or ball.x
	ball.y = y or ball.y
	
	ball.vx = vx or ball.vx
	ball.vy = vy or ball.vy
	
	ball.orig_vx = vx
	ball.orig_vy = vy
	
	ball.orig_x = x
	ball.orig_y = y
	
	ball.time = time or ball.time
	
	ball.visible = true
	
	return ball;
end

function resetBall(time)
	setBall(ball.x,ball.y,ball.vx*friction,ball.vy*friction,time)
end


function checkBumperCollision(bumper,real)

	-- real: if true, process gameplay events
	-- otherwise, just calculate vectors (for targeting trace)
	if real == nil then real = true end
	
	-- do simple bounding box check first for efficiency
	if (ball.x-ball.radius > bumper.x-bumper.radius and ball.x-ball.radius < bumper.x+bumper.radius
	and ball.y-ball.radius > bumper.y-bumper.radius and ball.y-ball.radius < bumper.y+bumper.radius) or
	   (ball.x+ball.radius > bumper.x-bumper.radius and ball.x+ball.radius < bumper.x+bumper.radius
	and ball.y-ball.radius > bumper.y-bumper.radius and ball.y-ball.radius < bumper.y+bumper.radius) or
	   (ball.x-ball.radius > bumper.x-bumper.radius and ball.x-ball.radius < bumper.x+bumper.radius
	and ball.y+ball.radius > bumper.y-bumper.radius and ball.y+ball.radius < bumper.y+bumper.radius) or
	   (ball.x+ball.radius > bumper.x-bumper.radius and ball.x+ball.radius < bumper.x+bumper.radius
	and ball.y+ball.radius > bumper.y-bumper.radius and ball.y+ball.radius < bumper.y+bumper.radius) then
	
		-- bounding box collides; check for circular collision

		-- distance below which collision occurs
		distance = ball.radius + bumper.radius
		
		actual_dist = math.sqrt( (bumper.x - ball.x)^2 + (bumper.y - ball.y)^2 )
		
		if actual_dist < distance then
			-- get ball velocity
			bv = math.sqrt(ball.vx^2 + ball.vy^2)
			-- get angle of incidence
			bi = math.atan2(ball.vy, ball.vx)
			-- get angle of normal
			normal = math.atan2(bumper.y - ball.y, bumper.x - ball.x)
			nx = math.cos(normal)
			ny = math.sin(normal)
			-- get angle of reflection
			br = bi + math.pi - ((bi-normal)*2)
			
			--set ball position to be not less than collision distance
			ball.x = bumper.x - (distance*nx)
			ball.y = bumper.y - (distance*ny)
			
			-- set new ball velocity
			ball.vx = math.cos(br)*bv
			ball.vy = math.sin(br)*bv
			
			if real and bumper.type ~= "active" and bumper.type ~= "fixed" and bumper.hit==0 then
				snd_bumperlit:play():frequency(22000+(hitcount*1000))
				hitcount = hitcount + 1
				bumper.hit = 1 -- lights up bumper, ready for later removal

				if bumper.type == 0 then
					bumperscore = redscore*scoremultiplier
				else
					bumperscore = normalscore*scoremultiplier
				end
					
				if bonus then
					bumperscore = bumperscore*bonusmultiplier
				end
				
				if not freeball then
					ballscore = ballscore + bumperscore
					if ballscore > freeballat then
						ballcount = ballcount + 1
						snd_extraball:play()
						freeball = true
					end
				end
				
				score = score + bumperscore
				
 
				if bumper.type == 2 then
					guidecount = guidecount + 3
				elseif bumper.type == 3 then
					bonus = true
				end
				
				
			elseif bumper.type == "active" then
				if real then
					snd_activehit:play()
					bumper.hit = 5 -- lights up active bumper, sets light countdown
				end
				
				-- add active bumper boost (along normal) to ball velocity 
				ball.vx = ball.vx - (activeboost * nx)
				ball.vy = ball.vy - (activeboost * ny)
			elseif real then
				if bumper.type == "fixed" then
					snd_fixedhit:play()
				else
					snd_bumpertouch:play()
				end
			end
			
			return true
	
		end
	
	end
	
	return false
end

function processBall(time,real)
		if real == nil then real = true end

		timediff = time - ball.time

		-- set ball's position at this time
		ball.x = ball.orig_x + ball.orig_vx*timediff
		ball.y = ball.orig_y + (ball.orig_vy*timediff)+(gravity*timediff*timediff/2)
		
		-- set ball's velocity at this time
		ball.vx = ball.orig_vx
		ball.vy = ball.orig_vy+(timediff*gravity)

		-- check for wall collisions
		if ball.y < (board.miny+ball.radius) and ball.vy < 0 then
			if real then snd_bumpertouch:play() end
			ball.vy = -ball.vy
			return true
		end
		if (ball.x > board.maxx-ball.radius and ball.vx > 0) or (ball.x < board.minx+ball.radius and ball.vx < 0) then
			if real then snd_bumpertouch:play() end
			ball.vx = -ball.vx
			return true
		end
		
		-- check for bumper collisions
		for i=1, # bumpers do
			if checkBumperCollision(bumpers[i],real) then
				return true
			end
		end

end



-- INITIALISATION

math.randomseed(os.time())

black = Color.new(0,0,0)
green = Color.new(0,255,0)
yellow = Color.new(255,255,0);
white = Color.new(255,255,255);
darkgreen = Color.new(0,64,0);
darkblue = Color.new(0,0,64);

img_splash = Image.load("data/neoflash.png")
screen:blit(0,0,img_splash)
screen:flip()
delay(90)

screen:clear(black)
screen:flip()
delay(30)

img_splash = Image.load("data/splash.png")
screen:blit(0,0,img_splash)
screen:flip()

-- Load resources
img_frame = Image.load("data/frame.png")
img_background = Image.load("data/background.png")
img_sprites = Image.load("data/sprites.png")
img_menu = Image.load("data/menu.png")
img_help = Image.load("data/help.png")

snd_bumperlit = Sound.load("data/bumperlit.wav")
snd_bumpertouch = Sound.load("data/clang.wav")
snd_activehit = Sound.load("data/typewritershift.wav")
snd_fixedhit = Sound.load("data/metalplate.wav")
snd_ready = Sound.load("data/gunreload.wav")
snd_fire = Sound.load("data/gunshot.wav")
snd_die = Sound.load("data/bomb2.wav")
snd_extraball = Sound.load("data/cashregister.wav")
snd_win = Sound.load("data/cheer.wav");
snd_lose = Sound.load("data/awww.wav");

font = Font.createMonoSpaced();
font:setPixelSizes(0,10);

bonusfont = Font.createMonoSpaced();
bonusfont:setPixelSizes(0,10);


-- distances in pixels
-- time in 'ticks'
soff = 6         -- shadow offset
mingap = 16      -- minimum gap between bumpers
gravity = 0.1    -- vertical acceleration due to gravity (pixels/tick)
friction = 0.8   -- velocity loss due to friction on collision
activeboost = 3  -- added velocity on active bumper collision (pixels/tick)
num_bumpers = 30
num_fixed = 3    -- number of fixed bumpers
num_active = 2   -- number of active bumpers
num_green = 3    -- number of guide bumpers
num_yellow = 2   -- number of bonus bumpers

normalscore = 100
redscore = 200
freeballscore = 500
scoremultiplier = 1
bonusmultiplier = 50
bonus = false
guidecount = 0

level = 1
score = 0
ballscore = 0    -- score for this ball only; used for calculating free ball

freeballat = 5000 -- ball score at which a free ball is awarded 
freeball = false

hitcount = 0

-- data structures
board = {minx = 25, maxx = 454, miny = 29, maxy = 262, minby = 70, maxby = 250}
cannon = {radius=32, x=240, y=28, angle=math.pi/2, limit=0.3, turn=0.05, slowturn=0.01, power=5}
catcher = {width=80,height=8,x=board.minx,y=board.maxy+2,vx=2}
trace = {count = 0, length = 25 }

-- animation sequences
bumperanim = { 0,0,0,1,1,1,2,2,2,3,3,3 }
ballanim =   { 1,1,2,2,3,3,4,4,5,5,6,6,7,7 }

time = 0

gamestate = "menu"


-- end splash
delay(45)
screen:clear(black)
screen:flip()
delay(60)
img_splash = nil



-- GAME LOOP

-- gamestate:
---- menu     - front screen
---- help     - game instructions
---- newlevel - sets up for as new level, populates the bumper table
---- ready    - game start, player can aim the cannon and fire
---- play     - ball in play, calculate physics & collisions each cycle
---- done     - ball exits play, show 'death' animation if applicable
---- remove   - remove hit bumpers from the bumper table
---- win      - end of level, start new level
---- end      - end of game, return to menu 
---- exit     - exits the program

snd_intro = Sound.load("data/intro.wav")
snd_intro:play();
snd_intro = nil;

lastcontrols = Controls.read()

while gamestate ~= "exit" do

	controls = Controls.read()
	
	if gamestate == "menu" then
		if controls ~= lastcontrols then
			if controls:cross() then
			
				-- reset for new game
				level = 0 -- +1 at new level
				score = 0
				ballcount = 6 -- +3 at new level
				
				snd_ready:play();				
				gamestate = "newlevel"
				
			elseif controls:triangle() then
				gamestate = "help"
			end
		end

	elseif gamestate == "help" then
		if controls ~= lastcontrols then
			if controls:buttons()>0 then
				gamestate = "menu"
			end
		end
	end
	
	
	-- cannon controls
	if controls:l() then
		turnspeed = cannon.slowturn
	else
		turnspeed = cannon.turn
	end
	
	if controls:left() then
		cannon.angle = cannon.angle + turnspeed
		if cannon.angle > (math.pi-cannon.limit) then
			cannon.angle = math.pi-cannon.limit
		end
	elseif controls:right() then
		cannon.angle = cannon.angle - turnspeed
		if cannon.angle < (cannon.limit) then
			cannon.angle = cannon.limit
		end
	end
	
	cx = math.cos(cannon.angle)
	cy = math.sin(cannon.angle)
	
	-- set barrel position
	cannon.bx = cannon.x+(cx*cannon.radius)
	cannon.by = cannon.y+(cy*cannon.radius)

	if gamestate == "newlevel" then
		level = level + 1
		guidecount = 0
		-- add 4 balls at start of new level
		ballcount = ballcount + 4
		bumpers = {}
		
		num_red = 9 + level;
		if num_red > num_bumpers-num_green-num_yellow then
			num_red = num_bumpers-num_green-num_yellow;
		end
		
		-- random level setup
		addRandomBumpers("fixed",num_fixed)
		addRandomBumpers("active",num_active)
		addRandomBumpers(0,num_red)
		addRandomBumpers(1,num_bumpers-num_red-num_green-num_yellow)
		addRandomBumpers(2,num_green)
		addRandomBumpers(3,num_yellow)
		
		-- set initial ball position
		setBall(cannon.bx, cannon.by,                    
		cx*cannon.power,cy*cannon.power,0)

		gamestate = "ready"
		
	elseif gamestate == "ready" then
		ballscore = 0;
		bonus = false;
		freeball = false;
		
		-- display low ball warning
		if ballwarning==nil and ballcount < 3 then
			ballwarning = 60
		end
		
		if (#bumpers - num_fixed - num_active) < (num_bumpers*0.25) then
			scoremultiplier = 10
		elseif (#bumpers - num_fixed - num_active) < (num_bumpers*0.5) then
			scoremultiplier = 5
		elseif (#bumpers - num_fixed - num_active) < (num_bumpers*0.75) then
			scoremultiplier = 2
		else
			scoremultiplier = 1
		end
		
		-- remove warning on any button press
		if controls ~= lastcontrols and controls:buttons() > 0 then
			ballwarning = 0
		end
		
		if controls ~= lastcontrols and controls:cross() then
			-- reset ball warning
			ballwarning = nil
			
			-- set ball's time of origin
			ball.time = time
			-- play fire sound
			snd_fire:play()
			gamestate = "play"
		else
			-- set ball start position
			setBall(cannon.bx, cannon.by,                    
			cx*cannon.power,cy*cannon.power,0)
			
			-- calculate trace
			trace.count = 0
			if guidecount > 0 and controls:r() then
				usedguide = true
				for i=1,trace.length do
					if processBall(i,false) then
						resetBall(i)
					end
					point = {x=ball.x,y=ball.y}
					trace[i] = point
					trace.count = trace.count + 1
					if ball.y > board.maxy-ball.radius then
						break
					end
				end
			end
		end
		
		-- reset ball position to cannon angle
		setBall(cannon.bx, cannon.by,
			cx*cannon.power,cy*cannon.power)
			
	elseif gamestate == "play" then
		reset = false -- resets ball state on collision
	
		-- process ball physics and check for collisions
		if processBall(time) then
			resetBall(time)
		end
	
		if ball.y > board.maxy-ball.radius then
			if (ball.x > catcher.x+ball.radius) and (ball.x < catcher.x+catcher.width-ball.radius) then

			    -- bonus points if ball returned after hitting some bumpers
				if ballscore > 0 then
					score = score + freeballscore
				end
			
				ball.visible = false
				ballcount = ballcount + 1
				snd_extraball:play()
				freeball = true
			else
				dead = true
				snd_die:play()
			end
			animframe = 1
			gamestate = "done"
		end
	
		
	elseif gamestate == "done" then
		-- animate ball death if appropriate
		animframe = animframe + 1
		if animframe>14 then
			ball.visible = false
			-- set up for bumper removal
			animframe = 1
			gamestate = "remove"
		end
	
		
	elseif gamestate == "remove" then
		-- increment animation
		animframe = animframe + 1
		if animframe>12 then
			animframe=1
			
			-- remove hit bumpers from table
			redcount = 0;
			new_bumpers = {}
			for key,bumper in pairs(bumpers) do
				if bumper.hit == 0 then
					if bumper.type == 0 then
						redcount = redcount + 1
					end
					table.insert(new_bumpers,bumper)
				end
			end
			bumpers = new_bumpers
			
			-- reset hit counter
			hitcount = 0
			
			if usedguide then
				guidecount = guidecount - 1
				usedguide = false
			end
			
			trace.count = 0
			
			if redcount == 0 then
				snd_win:play();
				gamestate = "win"
			else
				gamestate = "ready"
			end
			
			if ballcount > 0 then
				ballcount = ballcount -1
				snd_ready:play()
			elseif gamestate=="ready" then
				-- not finished, game over
				snd_lose:play();
				gamestate = "end"
			end

			
		end
	
	elseif gamestate == "win" then
		if controls ~= lastcontrols then
			if controls:buttons()>0 then
				gamestate = "newlevel"
			end
		end
	
	elseif gamestate == "end" then
		if controls ~= lastcontrols then
			if controls:buttons()>0 then
				gamestate = "menu"
			end
		end
	end
	
	if gamestate == "play" then
		time = time + 1
	end
	
	-- move the catcher in all gamestates
	catcher.x = catcher.x + catcher.vx
	if catcher.x >= (board.maxx-catcher.width) or catcher.x <= board.minx then
		catcher.vx = -catcher.vx
	end

	
	-- DRAW GRAPHICS
	
	if gamestate == "help" then
		-- help screen
		screen:blit(0,0,img_help)
	else
		-- frame
		screen:blit(0,0,img_frame)
		-- cannon barrel
		screen:blit(cannon.bx-8,cannon.by-8,img_sprites,112,48,16,16)
		-- background (hides rest of barrel)
		screen:blit(25,29,img_background)
	
		if gamestate == "menu" then
			-- menu overlay
			screen:blit(112,64,img_menu,0,0,256,192)
		else
			
			-- score
			scoretext = string.format("%10d",tostring(score));
			screen:fontPrint(font,380,18,scoretext,white);
			
			if freeball then
				screen:blit(-4,-2,img_sprites,0,96,32,32);
			end

			-- ball score
			screen:fillRect(8,34,8,(230-(230*ballscore/freeballat)),darkgreen);
			
			-- score multiplier
			screen:fillRect(464,34,8,(230*(#bumpers-num_fixed-num_active)/num_bumpers),darkblue);
			
			if bonus then
				if scoremultiplier == 2 then
					screen:blit(452,-2,img_sprites,32,128,32,32);
				elseif scoremultiplier == 5 then
					screen:blit(452,-2,img_sprites,64,128,32,32);
				elseif scoremultiplier == 10 then
					screen:blit(452,-2,img_sprites,96,128,32,32);
				else
					screen:blit(452,-2,img_sprites,0,128,32,32);
				end
			else
				if scoremultiplier == 2 then
					screen:blit(452,-2,img_sprites,32,96,32,32);
				elseif scoremultiplier == 5 then
					screen:blit(452,-2,img_sprites,64,96,32,32);
				elseif scoremultiplier == 10 then
					screen:blit(452,-2,img_sprites,96,96,32,32);
				end
			end
			
			if gamestate=="ready" and guidecount > 0 then
				if usedguide then
					screen:fontPrint(font,275,18,"Guides:"..guidecount,yellow);
				else
					screen:fontPrint(font,275,18,"Guides:"..guidecount.." (hold R)",green);
				end
			else
				screen:fontPrint(font,275,18,"Level "..level,white)
			end
			
			-- ball track
			drawballs = ballcount-1
			if drawballs > 13 then drawballs = 13 end
			for i=0,drawballs do
					screen:blit(196-(i*12),8,img_sprites,0,0,12,12)
			end
			
			-- ball catcher
			screen:blit(catcher.x,catcher.y,img_sprites,0,24,80,8)
			
			-- ball
			if ball.visible then
				if(gamestate == "ready" or gamestate == "play") then
					-- shadow
					screen:blit(ball.x-6+soff,ball.y-6+soff,img_sprites,0,12,12,12)
					-- sprite
					screen:blit(ball.x-6,ball.y-6,img_sprites,0,0,12,12)
				end
				if gamestate == "done" then
					-- sprite
					screen:blit(ball.x-6,ball.y-18,img_sprites,ballanim[animframe]*12,0,12,24)
				end
			end
			
			-- bumpers
			for i=1, # bumpers do
				-- shadow
				if bumpers[i].type == "fixed" or bumpers[i].type == "active" then
					screen:blit(bumpers[i].x-bumpers[i].radius+soff,bumpers[i].y-bumpers[i].radius+soff,img_sprites,96,64,32,32)
				else
					screen:blit(bumpers[i].x-bumpers[i].radius+soff,bumpers[i].y-bumpers[i].radius+soff,img_sprites,64,48,16,16)
				end
				-- sprite
				if bumpers[i].type == "fixed" then
					screen:blit(bumpers[i].x-bumpers[i].radius,bumpers[i].y-bumpers[i].radius,img_sprites,0,64,32,32)
				elseif bumpers[i].type == "active" then
					if bumpers[i].hit>0 then
						screen:blit(bumpers[i].x-bumpers[i].radius,bumpers[i].y-bumpers[i].radius,img_sprites,64,64,32,32)
						-- decrement animation counter
						bumpers[i].hit = bumpers[i].hit - 1
					else
						screen:blit(bumpers[i].x-bumpers[i].radius,bumpers[i].y-bumpers[i].radius,img_sprites,32,64,32,32)
					end
				else
					if gamestate=="remove" then
						if bumpers[i].hit == 1 then
							screen:blit(bumpers[i].x-bumpers[i].radius,bumpers[i].y-bumpers[i].radius,img_sprites,bumperanim[animframe]*16+64,32,16,16)
						else 
							screen:blit(bumpers[i].x-bumpers[i].radius,bumpers[i].y-bumpers[i].radius,img_sprites,bumpers[i].type*16,32,16,16)
						end
					else 
						screen:blit(bumpers[i].x-bumpers[i].radius,bumpers[i].y-bumpers[i].radius,img_sprites,bumpers[i].type*16,bumpers[i].hit*16+32,16,16)
					end
				end
			end
			
			if gamestate=="ready" then
				-- draw trace
				for i=2,trace.count do
					--screen:fillRect(trace[i].x,trace[i].y,2,2,green)
					screen:drawLine(trace[i].x,trace[i].y,trace[i-1].x,trace[i-1].y,green)
				end
				
				if ballwarning and ballwarning > 0 then
					ballwarning = ballwarning - 1
					if ballcount == 2 then
						screen:blit(112,64,img_menu,0,288,256,32)
					elseif ballcount == 1 then
						screen:blit(112,64,img_menu,0,320,256,32)
					elseif ballcount == 0 then
						screen:blit(112,64,img_menu,0,352,256,32)
					end
				end
				
			elseif gamestate == "win" then
				-- "level cleared!"
				screen:blit(112,64,img_menu,0,192,256,32)
				screen:blit(112,220,img_menu,0,256,256,32)
				
			elseif gamestate == "end" then
				-- "game over"
				screen:blit(112,64,img_menu,0,224,256,32)
				screen:blit(112,220,img_menu,0,256,256,32)
				
			end
			
		end
		
	end

	screen:waitVblankStart()
	screen:flip()
	
	lastcontrols = controls
end
	
	
