# Tankspank
# kevin conner
# connerk@gmail.com
# version 2, 1 March 2008
# this code is free, do what you like with it!

$width, $height = 700, 400
$camera_tightness = 0.1

class Building
	def initialize(west, east, north, south)
		@west, @east, @north, @south = west, east, north, south
		@top, @bottom = 1.1 + rand(3) * 0.15, 1.0
		
		color = (1..3).collect { 0.2 + 0.4 * rand }
		color << 0.9
		@stroke = $app.rgb *color
		color[-1] = 0.3
		@fill = $app.rgb *color
	end
	
	def draw
		$app.stroke @stroke
		$app.fill @fill
		Opp.draw_opp_box(@west, @east, @north, @south, @top, @bottom)
	end
end

module Guidable
	def guidance_system x, y, dest_x, dest_y, angle
		vx, vy = dest_x - x, dest_y - y
		if vx.abs < 0.1 and vy.abs <= 0.1
			yield 0, 0
		else
			length = Math.sqrt(vx * vx + vy * vy)
			vx /= length
			vy /= length
			ax, ay = Math.cos(angle), Math.sin(angle)
			cos_between = vx * ax + vy * ay
			sin_between = vx * -ay + vy * ax
			yield sin_between, cos_between
		end
	end
end

class Tank
	include Guidable
	
	def initialize
		@x, @y = 0, 0
		@tank_angle = 0.0
		@dest_x, @dest_y = 0, 0
		@acceleration = 0.0
		@speed = 0.0
		@moving = false
		
		@aim_angle = 0.0
		@target_x, @target_y = 0, 0
		@aimed = false
	end
	
	attr_reader :x, :y
	
	def mark_destination
		@dest_x, @dest_y = @target_x, @target_y
		@moving = true
	end
	
	def fire
		Opp.add_shell Shell.new @x + 30 * Math.cos(@aim_angle),
			@y + 30 * Math.sin(@aim_angle), @aim_angle
	end
	
	def update button, mouse_x, mouse_y
		@target_x, @target_y = mouse_x, mouse_y
		
		if @moving
			guidance_system @x, @y, @dest_x, @dest_y, @tank_angle do |direction, on_target|
				turn direction
				@acceleration = on_target * 0.5
			end
			
			distance = Math.sqrt((@dest_x - @x) ** 2 + (@dest_y - @y) ** 2)
			@moving = false if distance < 50
		else
			@acceleration = 0.0
		end
		
		guidance_system @x, @y, @target_x, @target_y, @aim_angle do |direction, on_target|
			aim direction
			@aimed = on_target > 0.98
		end
		
		@speed = [[@speed + @acceleration, 5.0].min, -3.0].max
		@speed *= 0.9 if !@moving
		
		@x += @speed * Math.cos(@tank_angle)
		@y += @speed * Math.sin(@tank_angle)
	end
	
	def turn direction
		@tank_angle += [[-0.03, direction].max, 0.03].min
	end
	
	def aim direction
		@aim_angle += [[-0.1, direction].max, 0.1].min
	end
	
	def draw
		$app.stroke $app.blue
		$app.fill $app.blue 0.4
		Opp.draw_opp_rect @x - 20, @x + 20, @y - 15, @y + 15, 1.05, @tank_angle
		#Opp.draw_opp_box @x - 20, @x + 20, @y - 20, @y + 20, 1.03, 1.0
		Opp.draw_opp_rect @x - 10, @x + 10, @y - 7, @y + 7, 1.05, @aim_angle
		x, unused1, y, unused2 = Opp.project(@x, 0, @y, 0, 1.05)
		$app.line x, y, x + 25 * Math.cos(@aim_angle), y + 25 * Math.sin(@aim_angle)
		
		$app.stroke $app.red
		$app.fill $app.red(@aimed ? 0.4 : 0.1)
		Opp.draw_opp_oval @target_x - 10, @target_x + 10, @target_y - 10, @target_y + 10, 1.00
		
		if @moving
			$app.stroke $app.green
			$app.fill $app.green 0.2
			Opp.draw_opp_oval @dest_x - 20, @dest_x + 20, @dest_y - 20, @dest_y + 20, 1.00
		end
	end
end

class Shell
	def initialize x, y, angle
		@x, @y, @angle = x, y, angle
		@speed = 10.0
	end
	
	def update
		@x += @speed * Math.cos(@angle)
		@y += @speed * Math.sin(@angle)
	end
	
	def draw
		$app.stroke $app.red
		$app.fill $app.red(0.1)
		Opp.draw_opp_box @x - 2, @x + 2, @y - 2, @y + 2, 1.05, 1.04
	end
end

class Opp
	def self.new_game
		@offset_x, @offset_y = 0, 0
		@buildings = [
			[-1000, -750, -750, -250],
			[-500, 250, -750, -250],
			[500, 1000, -750, -500],
			[750, 1250, -250, 0],
			[750, 1250, 250, 750],
			[250, 500, 0, 750],
			[-250, 0, 0, 500],
			[-500, 0, 750, 1000],
			[-1000, -500, 0, 500],
			[400, 600, -350, -150]
		].collect { |p| Building.new *p }
		@shells = []
		@boundary = [-1250, 1500, -1250, 1250]
		@tank = Tank.new
		@center_x, @center_y = $app.width / 2, $app.height / 2
		#todo other stuff
	end
	
	def self.tank
		@tank
	end
	
	def self.update_scene
		button, x, y = $app.mouse
		x += @offset_x - @center_x
		y += @offset_y - @center_y
		@tank.update button, x, y
		@shells.each { |s| s.update }
		
		$app.clear do
			@offset_x += $camera_tightness * (@tank.x - @offset_x)
			@offset_y += $camera_tightness * (@tank.y - @offset_y)
			
			$app.background $app.black
			@center_x, @center_y = $app.width / 2, $app.height / 2
			
			$app.stroke $app.red(0.9)
			$app.nofill
			draw_opp_box *(@boundary + [1.1, 1.0, false])
			
			@tank.draw
			@shells.each { |s| s.draw }
			@buildings.each { |b| b.draw }
		end
	end
	
	def self.add_shell shell
		@shells << shell
		@shells.shift if @shells.size > 10
	end
	
	def self.project left, right, top, bottom, depth
		[left, right].collect { |x| @center_x + depth * (x - @offset_x) } +
			[top, bottom].collect { |y| @center_y + depth * (y - @offset_y) }
	end
	
	# here "front" and "back" push the rect into and out of the window.
	# 1.0 means your x and y units are pixels on the surface.
	# greater than that brings the box closer.  less pushes it back.  0.0 => infinity.
	# the front will be filled but the rest is wireframe only.
	def self.draw_opp_box left, right, top, bottom, front, back, occlude = true
		near_left, near_right, near_top, near_bottom = project(left, right, top, bottom, front)
		far_left, far_right, far_top, far_bottom = project(left, right, top, bottom, back)
		
		# determine which sides of the box are visible
		if occlude
			draw_left = @center_x < near_left
			draw_right = near_right < @center_x
			draw_top = @center_y < near_top
			draw_bottom = near_bottom < @center_y
		else
			draw_left, draw_right, draw_top, draw_bottom = [true] * 4
		end
		
		# draw lines for the back edges
		$app.line far_left, far_top, far_right, far_top if draw_top
		$app.line far_left, far_bottom, far_right, far_bottom if draw_bottom
		$app.line far_left, far_top, far_left, far_bottom if draw_left
		$app.line far_right, far_top, far_right, far_bottom if draw_right
		
		# draw lines to connect the front and back
		$app.line near_left, near_top, far_left, far_top if draw_left or draw_top
		$app.line near_right, near_top, far_right, far_top if draw_right or draw_top
		$app.line near_left, near_bottom, far_left, far_bottom if draw_left or draw_bottom
		$app.line near_right, near_bottom, far_right, far_bottom if draw_right or draw_bottom
		
		# draw the front, filled
		$app.rect near_left, near_top, near_right - near_left, near_bottom - near_top
	end
	
	def self.draw_opp_rect left, right, top, bottom, depth, angle, with_x = false
		pl, pr, pt, pb = project(left, right, top, bottom, depth)
		cos = Math.cos(angle)
		sin = Math.sin(angle)
		cx, cy = (pr + pl) / 2.0, (pb + pt) / 2.0
		points = [[pl, pt], [pr, pt], [pr, pb], [pl, pb]].collect do |x, y|
			[cx + (x - cx) * cos - (y - cy) * sin,
				cy + (x - cx) * sin + (y - cy) * cos]
		end
		
		$app.line *(points[0] + points[1])
		$app.line *(points[1] + points[2])
		$app.line *(points[2] + points[3])
		$app.line *(points[3] + points[0])
	end
	
	def self.draw_opp_oval left, right, top, bottom, depth
		pl, pr, pt, pb = project(left, right, top, bottom, depth)
		$app.oval(pl, pt, pr - pl, pb - pt)
	end
	
	def self.draw_opp_plane x1, y1, x2, y2, front, back, stroke_color
		near_x1, near_x2, near_y1, near_y2 = project(x1, x2, y1, y2, front)
		far_x1, far_x2, far_y1, far_y2 = project(x1, x2, y1, y2, back)
		
		$app.stroke stroke_color
		
		$app.line far_x1, far_y1, far_x2, far_y2
		$app.line far_x1, far_y1, near_x1, near_y1
		$app.line far_x2, far_y2, near_x2, near_y2
		$app.line near_x1, near_y1, near_x2, near_y2
	end
end

Shoes.app :width => $width, :height => $height do
	$app = self
	
	Opp.new_game
	
	keypress do |key|
		if key == "1" or key == "z"
			Opp.tank.mark_destination
		elsif key == "2" or key == "x" or key == " "
			Opp.tank.fire
		end
	end
	
	click do |button, x, y|
		if button == 1
			Opp.tank.mark_destination
		else
			Opp.tank.fire
		end
	end
	
	animate(60) { Opp.update_scene }
end
