#############################################################################
#
# Copyright (c) 2010 by Casey Duncan and contributors
# All Rights Reserved.
#
# This software is subject to the provisions of the MIT License
# A copy of the license should accompany this distribution.
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
#
#############################################################################
"""Basic vector graphics arcade game built with Grease"""

__version__ = '$Id: blasteroids.py 584 2010-02-28 06:56:44Z casey.duncan $'

import os
import math
import random
import itertools
import pyglet
from pyglet.window import key
import grease
from grease import component, controller, geometry, collision, renderer, mode
from grease.pygletsys import KeyControls

## Utility functions ##

SCRIPT_DIR_PATH = os.path.dirname(__file__)
pyglet.font.add_file(SCRIPT_DIR_PATH + '/font/Vectorb.ttf')

def load_sound(name, streaming=False):
	"""Load a sound from the `sfx` directory"""
	return pyglet.media.load(
		'%s/sfx/%s' % (SCRIPT_DIR_PATH, name), streaming=streaming)

def looping_sound(name):
	"""Load a sound from the `sfx` directory and configure it too loop
	continuously
	"""
	player = pyglet.media.Player()
	player.queue(load_sound(name, streaming=True))
	player.eos_action = player.EOS_LOOP
	return player

## Define entity classes ##

class BlasteroidsEntity(grease.Entity):
	"""Entity base class"""

	def explode(self):
		"""Segment the entity shape into itty bits"""
		shape = self.shape.verts.transform(angle=self.position.angle)
		for segment in shape.segments():
			debris = Debris(self.world)
			debris.shape.verts = segment
			debris.position.position = self.position.position
			debris.movement.velocity = self.movement.velocity
			debris.movement.velocity += segment[0].normalized() * random.gauss(50, 20)
			debris.movement.rotation = random.gauss(0, 45)
			debris.renderable.color = self.renderable.color


class Debris(BlasteroidsEntity):
	"""Floating space junk"""


class PlayerShip(BlasteroidsEntity):
	"""Thrust ship piloted by the player"""

	THRUST_ACCEL = 75
	TURN_RATE = 240
	SHAPE_VERTS = [
		(-8, -12), (-4, -10), (0, -8), (4, -10), (8, -12), # flame
		(0, 12), (-8, -12), (0, -8), (8, -12)]
	COLOR = (0.5, 1, 0.5)
	GUN_COOL_DOWN = 0.5
	GUN_SOUND = load_sound('pewpew.wav')
	THRUST_SOUND = looping_sound('thrust.wav')
	DEATH_SOUND = load_sound('dead.wav')
	COLLISION_RADIUS = 7.5
	COLLIDE_INTO_MASK = 0x1

	def __init__(self, world, invincible=False):
		self.position.position = (0, 0)
		self.position.angle = 0
		self.movement.velocity = (0, 0)
		self.movement.rotation = 0
		self.shape.verts = self.SHAPE_VERTS
		self.shape.closed = False
		self.collision.radius = 7.5
		self.renderable.color = self.COLOR
		self.gun.firing = False
		self.gun.last_fire_time = 0
		self.gun.cool_down = self.GUN_COOL_DOWN
		self.gun.sound = self.GUN_SOUND
		self.set_invincible(invincible)
	
	def turn(self, direction):
		self.movement.rotation = self.TURN_RATE * direction
	
	def thrust_on(self):
		thrust_vec = geometry.Vec2d(0, self.THRUST_ACCEL)
		thrust_vec.rotate(self.position.angle)
		self.movement.accel = thrust_vec
		self.shape.verts[2] = geometry.Vec2d(0, -16 - random.random() * 16)
		self.THRUST_SOUND.play()
	
	def thrust_off(self):
		self.movement.accel = geometry.Vec2d(0, 0)
		self.shape.verts[2] = geometry.Vec2d(0, -8)
		self.THRUST_SOUND.pause()
	
	def set_invincible(self, invincible):
		"""Set the invincibility status of the ship. If invicible is
		True then the ship will not collide with any obstacles and will
		blink to indicate this. If False, then the normal collision 
		behavior is restored
		"""
		if invincible:
			self.collision.into_mask = 0
			self.collision.from_mask = 0
			self.world.clock.schedule_interval(self.blink, 0.15)
			self.world.clock.schedule_once(lambda dt: self.set_invincible(False), 3)
		else:
			self.world.clock.unschedule(self.blink)
			self.renderable.color = self.COLOR
			self.collision.from_mask = 0xffffffff
			self.collision.into_mask = self.COLLIDE_INTO_MASK
	
	def blink(self, dt):
		"""Blink the ship to show invincbility"""
		if self.renderable:
			del self.renderable
		else:
			self.renderable.color = self.COLOR
	
	def on_collide(self, other, point, normal):
		self.explode()
		self.DEATH_SOUND.play()
		self.world.systems.game.player_died()


class Asteroid(BlasteroidsEntity):
	"""Big floating space rock"""

	COLLIDE_INTO_MASK = 0x2
	HIT_SOUNDS = [
		load_sound('hit1.wav'),
		load_sound('hit2.wav'),
		load_sound('hit3.wav'),
	]

	UNIT_CIRCLE = [(math.sin(math.radians(a)), math.cos(math.radians(a))) 
		for a in range(0, 360, 18)]
	
	def __init__(self, world, radius=45, position=None, parent_velocity=None, points=25):
		if position is None:
			self.position.position = (
				random.choice([-1, 1]) * random.randint(50, window.width / 2), 
				random.choice([-1, 1]) * random.randint(50, window.height / 2))
		else:
			self.position.position = position
		deviation = radius / 7
		self.movement.velocity = (random.gauss(0, 700 / radius), random.gauss(0, 700 / radius))
		if parent_velocity is not None:
			self.movement.velocity += parent_velocity
		self.movement.rotation = random.gauss(0, 15)
		verts = [(random.gauss(x*radius, deviation), random.gauss(y*radius, deviation))
			for x, y in self.UNIT_CIRCLE]
		self.shape.verts = verts
		self.collision.radius = radius
		self.collision.from_mask = PlayerShip.COLLIDE_INTO_MASK
		self.collision.into_mask = self.COLLIDE_INTO_MASK
		self.renderable.color = (0.75, 0.75, 0.75)
		self.award.points = points

	def on_collide(self, other, point, normal):
		if self.collision.radius > 15:
			chunk_size = self.collision.radius / 2.0
			for i in range(2):
				Asteroid(self.world, chunk_size, self.position.position, 
					self.movement.velocity, self.award.points * 2)
		random.choice(self.HIT_SOUNDS).play()
		self.explode()
		self.delete()	


class Shot(grease.Entity):
	"""Pew Pew!
	
	Args:
		`shooter` (Entity): entity that is shooting the shot. Used
		to determine the collision mask, position and velocity 
		so the shot doesn't hit the shooter.
		
		`angle` (float): Angle of the shot trajectory in degrees.
	"""

	SPEED = 300
	TIME_TO_LIVE = 0.75 # seconds
	
	def __init__(self, world, shooter, angle):
		offset = geometry.Vec2d(0, shooter.collision.radius)
		offset.rotate(angle)
		self.position.position = shooter.position.position + offset
		self.movement.velocity = (
			offset.normalized() * self.SPEED + shooter.movement.velocity)
		self.shape.verts = [(0, 1.5), (1.5, -1.5), (-1.5, -1.5)]
		self.collision.radius = 2.0
		self.collision.from_mask = ~shooter.collision.into_mask
		self.renderable.color = (1.0, 1.0, 0.95)
		world.clock.schedule_once(self.expire, self.TIME_TO_LIVE)

	def on_collide(self, other, point, normal):
		self.world.systems.game.award_points(other)
		self.delete()
	
	def expire(self, dt):
		self.delete()


class HudEntity(grease.Entity):
	"""Entities used by the HUD"""

## Define game systems ##

class PositionWrapper(grease.System):
	"""Wrap positions around when they go off the edge of the window"""

	def __init__(self):
		self.half_width = window.width / 2
		self.half_height = window.height / 2

	def step(self, dt):
		for entity in self.world[...].collision.aabb.right < -self.half_width:
			entity.position.position.x += window.width + entity.collision.aabb.width
		for entity in self.world[...].collision.aabb.left > self.half_width:
			entity.position.position.x -= window.width + entity.collision.aabb.width
		for entity in self.world[...].collision.aabb.top < -self.half_height:
			entity.position.position.y += window.height + entity.collision.aabb.height 
		for entity in self.world[...].collision.aabb.bottom > self.half_height:
			entity.position.position.y -= window.height + entity.collision.aabb.height


class Gun(grease.System):
	"""Fires Shot entities"""

	def step(self, dt):
		for entity in self.world[...].gun.firing == True:
			if self.world.time >= entity.gun.last_fire_time + entity.gun.cool_down:
				Shot(self.world, entity, entity.position.angle)
				if entity.gun.sound is not None:
					entity.gun.sound.play()
				entity.gun.last_fire_time = self.world.time


class Sweeper(grease.System):
	"""Clears out space debris"""

	SWEEP_TIME = 2.0

	def step(self, dt):
		fade = dt / self.SWEEP_TIME
		for entity in tuple(self.world[Debris].entities):
			color = entity.renderable.color
			if color.a > 0.2:
				color.a = max(color.a - fade, 0)
			else:
				entity.delete()


class GameSystem(KeyControls):
	"""Main game logic system

	This subclass KeyControls so that the controls can be bound
	directly to the game logic here
	"""

	CHIME_SOUNDS = [
		load_sound('chime1.wav'), 
		load_sound('chime2.wav'),
	]
	MAX_CHIME_TIME = 2.0
	MIN_CHIME_TIME = 0.6

	def set_world(self, world):
		KeyControls.set_world(self, world)
		self.level = 0
		self.lives = 3
		self.score = 0
		self.player_ship = PlayerShip(self.world)
		self.start_level()
	
	def start_level(self):
		self.level += 1
		for i in range(self.level * 3 + 1):
			Asteroid(self.world)
		self.chime_time = self.MAX_CHIME_TIME
		self.chimes = itertools.cycle(self.CHIME_SOUNDS)
		if self.level == 1:
			self.chime()
	
	def award_points(self, entity):
		"""Get points for destroying stuff"""
		if hasattr(entity.award, 'points'):
			self.score += entity.award.points
	
	def player_died(self):
		self.lives -= 1
		self.player_ship.THRUST_SOUND.pause()
		self.player_ship.delete()
		self.world.clock.schedule_once(self.player_respawn, 3.0)
		
	def player_respawn(self, dt=None):
		"""Rise to fly again, with temporary invincibility"""
		if self.lives:
			self.player_ship = PlayerShip(self.world, invincible=True)
		if self.world.is_multiplayer:
			# Switch to the next player
			if self.lives:
				window.current_mode.activate_next()
			else:
				window.current_mode.remove_submode()


	def chime(self, dt=0):
		"""Play tension building chime sounds"""
		if self.lives:
			self.chimes.next().play()
			self.chime_time = max(self.chime_time - dt * 0.01, self.MIN_CHIME_TIME)
			if not self.world[Asteroid].entities:
				self.start_level()
			self.world.clock.schedule_once(self.chime, self.chime_time)
	
	@KeyControls.key_press(key.LEFT)
	@KeyControls.key_press(key.A)
	def start_turn_left(self):
		if self.player_ship.exists:
			self.player_ship.turn(-1)

	@KeyControls.key_release(key.LEFT)
	@KeyControls.key_release(key.A)
	def stop_turn_left(self):
		if self.player_ship.exists and self.player_ship.movement.rotation < 0:
			self.player_ship.turn(0)

	@KeyControls.key_press(key.RIGHT)
	@KeyControls.key_press(key.D)
	def start_turn_right(self):
		if self.player_ship.exists:
			self.player_ship.turn(1)

	@KeyControls.key_release(key.RIGHT)
	@KeyControls.key_release(key.D)
	def stop_turn_right(self):
		if self.player_ship.exists and self.player_ship.movement.rotation > 0:
			self.player_ship.turn(0)
	
	@KeyControls.key_hold(key.UP)
	@KeyControls.key_hold(key.W)
	def thrust(self, dt):
		if self.player_ship.exists:
			self.player_ship.thrust_on()
		
	@KeyControls.key_release(key.UP)
	@KeyControls.key_release(key.W)
	def stop_thrust(self):
		if self.player_ship.exists:
			self.player_ship.thrust_off()
			
	@KeyControls.key_press(key.SPACE)
	def start_firing(self):
		if self.player_ship.exists:
			self.player_ship.gun.firing = True

	@KeyControls.key_release(key.SPACE)
	def stop_firing(self):
		if self.player_ship.exists:
			self.player_ship.gun.firing = False
	
	@KeyControls.key_press(key.P)
	def pause(self):
		self.world.running = not self.world.running	

	def on_key_press(self, key, modifiers):
		"""Start the world with any key if paused"""
		KeyControls.on_key_press(self, key, modifiers)
		if not self.world.running:
			self.world.running = True



class Hud(grease.Renderer):
	"""Heads-up display renderer"""
	
	HUD_FONT = pyglet.font.load('Vector Battle')

	def set_world(self, world):
		self.world = world
		self.last_lives = 0
		self.last_score = None
		self.game_over_label = None
		self.paused_label = None
		self.create_lives_entities()

	def draw(self):
		game = self.world.systems.game
		if self.last_lives != game.lives:
			for i, entity in self.lives:
				if game.lives > i:
					entity.renderable.color = PlayerShip.COLOR
				else:
					entity.renderable.color = (0,0,0,0)
			self.last_lives = game.lives
		if self.last_score != game.score:
			self.score_label = pyglet.text.Label(
				str(game.score),
				color=(180, 180, 255, 255),
				font_name='Vector Battle', font_size=14, bold=True,
				x=window.width // 2 - 25, y=window.height // 2 - 25, 
				anchor_x='right', anchor_y='center')
			self.last_score = game.score
		if game.lives == 0:
			if self.game_over_label is None:
				self.game_over_label = pyglet.text.Label(
					"GAME OVER",
					font_name='Vector Battle', font_size=36, bold=True,
					color=(255, 0, 0, 255),
					x = 0, y = 0, anchor_x='center', anchor_y='center')
			self.game_over_label.draw()
		if not self.world.running:
			if self.paused_label is None:
				self.player_label = pyglet.text.Label(
					self.world.player_name,
					color=(150, 150, 255, 255),
					font_name='Vector Battle', font_size=18, bold=True,
					x = 0, y = 20, anchor_x='center', anchor_y='bottom')
				self.paused_label = pyglet.text.Label(
					"press a key to begin",
					color=(150, 150, 255, 255),
					font_name='Vector Battle', font_size=16, bold=True,
					x = 0, y = -20, anchor_x='center', anchor_y='top')
			self.player_label.draw()
			self.paused_label.draw()
		self.score_label.draw()
	
	def create_lives_entities(self):
		"""Create entities to represent the remaining lives"""
		self.lives = []
		verts = geometry.Vec2dArray(PlayerShip.SHAPE_VERTS[3:])
		left = -window.width // 2 + 25
		top = window.height // 2 - 25
		for i in range(20):
			entity = HudEntity(self.world)
			entity.shape.verts = verts.transform(scale=0.75)
			entity.position.position = (i * 20 + left, top)
			self.lives.append((i, entity))


class TitleScreenControls(KeyControls):
	"""Title screen key event handler system"""

	@KeyControls.key_press(key._1)
	def start_single_player(self):
		window.push_mode(Game())
	
	@KeyControls.key_press(key._2)
	def start_two_player(self):
		window.push_mode(mode.Multi(
			Game('Player One'), 
			Game('Player Two')))


class BaseWorld(grease.World):

	def configure(self):
		"""Configure the game world's components, systems and renderers"""
		self.components.position = component.Position()
		self.components.movement = component.Movement()
		self.components.shape = component.Shape()
		self.components.renderable = component.Renderable()
		self.components.collision = component.Collision()
		self.components.gun = component.Component(
			firing=bool, 
			last_fire_time=float, 
			cool_down=float, 
			sound=object)
		self.components.award = component.Component(points=int)

		self.systems.movement = controller.EulerMovement()
		self.systems.collision = collision.Circular(
			handlers=[collision.dispatch_events])
		self.systems.wrapper = PositionWrapper()
		self.systems.gun = Gun()
		self.systems.sweeper = Sweeper()

		self.renderers.camera = renderer.Camera(
			position=(window.width / 2, window.height / 2))
		self.renderers.vector = renderer.Vector(line_width=1.5)


class TitleScreen(BaseWorld):
	"""Game title screen world and mode"""
	
	def configure(self):
		BaseWorld.configure(self)
		self.renderers.title = pyglet.text.Label(
			"Blasteroids",
			color=(150, 150, 255, 255),
			font_name='Vector Battle', font_size=32, bold=True,
			x=0, y=50, anchor_x='center', anchor_y='bottom')
		self.renderers.description = pyglet.text.Label(
			"A demo for the Grease game engine",
			color=(150, 150, 255, 255),
			font_name='Vector Battle', font_size=16, bold=True,
			x=0, y=20, anchor_x='center', anchor_y='top')
		self.renderers.one_player = pyglet.text.Label(
			"Press 1 for one player",
			color=(150, 150, 255, 255),
			font_name='Vector Battle', font_size=16, bold=True,
			x=0, y=-100, anchor_x='center', anchor_y='top')
		self.renderers.two_player = pyglet.text.Label(
			"Press 2 for two players",
			color=(150, 150, 255, 255),
			font_name='Vector Battle', font_size=16, bold=True,
			x=0, y=-130, anchor_x='center', anchor_y='top')

		self.systems.controls = TitleScreenControls()
		for i in range(15):
			Asteroid(self, radius=random.randint(12, 45))


class Game(BaseWorld):
	"""Main game world and mode"""

	is_multiplayer = False
	"""Flag to indicate if this is a multiplayer session"""

	def __init__(self, player_name=""):
		BaseWorld.__init__(self)
		self.player_name = player_name
		if player_name:
			# In multiplayer, start paused
			self.running = False
			self.is_multiplayer = True
	
	def configure(self):
		BaseWorld.configure(self)
		self.systems.game = GameSystem()
		self.renderers.hud = Hud()
	
	def deactivate(self, manager):
		grease.World.deactivate(self, manager)
		# Pause the game so if resumed, we begin paused
		self.running = False


def main():
	"""Initialize and run the game"""
	global window
	window = mode.ManagerWindow()
	window.push_mode(TitleScreen())
	pyglet.app.run()

if __name__ == '__main__':
	main()

