"""game_map.py

   Map display utilities

   Adventures on Planet Zephulor by Aaron Hachez. 

   All code, artwork, and sound copyright 2003, Aaron Hachez.

   ----------------------------------------------------------------------------------------

   This file is part of "Adventures on Planet Zephulor".

   A copy of the GNU General Public License should be included with any
   redistribution of this program in a file named "COPYING.txt".

   "Adventures on Planet Zephulor" is free software; you can redistribute it and/or modify
   it under the terms of the GNU General Public License as published by
   the Free Software Foundation; either version 2 of the License, or
   (at your option) any later version.

   "Adventures on Planet Zephulor" is distributed in the hope that it will be useful,
   but WITHOUT ANY WARRANTY; without even the implied warranty of
   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
   GNU General Public License for more details.

   You should have received a copy of the GNU General Public License
   along with "Adventures on Planet Zephulor"; if not, write to the Free Software
   Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA

   ----------------------------------------------------------------------------------------

   "Adventures on Planet Zephulor" uses the following libraries:

   PyGame
   SDL

   Both are at the time of this writing, licensed under the GNU LGPL. Please see
   the included file LGPL for details 
"""


import os
import myimg
import pygame
import cPickle
import game_entities
import game_projectiles

from game_config import config

class map_base:
    """Basic map class, includes width/height (in pixels), x/yvel, viewport(rect),
    viewportx/yoff, maplist (list of tiles to use and where),
    maptiles (A list of images), and map data (Data about map."""

    # The filename for the map
    mapfile = "default.data"

    # The list of maps to load at level end(s)
    nextmaplist = {}

    # The actual map to load at level end (Determined when player collides with end level tagged tile)
    nextmap = None

    # The file to use for music
    musicfile = None

    # The tile file
    tilefile = "default.bmp"

    # The tag name to use for background images
    bgtag = "default.background"

    # The extension to use for background images
    bgext = ".bmp"

    # The image name to use for the background, note, scrolling is determined by image size
    # if the image is small, it will be scrolled less than a large image.
    # At init time it is simply the tag name, when loading, a number and extension will be appended
    bgimg0 = bgtag
    bgimg1 = bgtag
    bgimg2 = bgtag

    # The ratio to use for movement of said background
    bgratio0x = 0
    bgratio0y = 0
    bgratio1x = 0
    bgratio1y = 0
    bgratio2x = 0
    bgratio2y = 0

    # The widths of said bg images.
    bgwidth0 = 0
    bgheight0 = 0
    bgwidth1 = 0
    bgheight1 = 0
    bgwidth2 = 0
    bgheight2 = 0

    # The current state, 1 = player death, 2 = player read, 3 = level end
    state = 0

    # Total map width and height in pixels
    width = 0
    height = 0

    # The number of tiles in a row/col of the map
    tileswide = 0
    tileshigh = 0

    # Tile width and height
    tilewidth = 0
    tileheight = 0

    # The offset to add at the end of each displayed row of tiles
    mapoffset = 0

    # The last beging tile used for draw and coldet routines
    lastrangestart = 0

    # The current list of tiles to use for draw and coldet routines
    currentlist = None

    # x/y velocity, i.e. screen scroll speed
    xvel = 0
    yvel = 0

    # Current viewport rect
    viewport = None

    # Viewport width/height in pixels
    viewportwidth = 0
    viewportheight = 0

    # Viewport width/height in tiles
    viewporttilewidth = 0
    viewporttileheight = 0

    # Flag for debugging, if not set, collision highlights will not be drawn
    coldebug = 0

    # lists used for highlighting tiles, specifically for troubleshooting
    redblist = None
    blueblist = None
    greenblist = None

    # A list of tiles to use for this map
    maplist = None

    # A list of map tiles (i.e. images)
    maptiles = None

    # A list for data pertaining to each tile
    # List is passable, passleft, passtop, passright, passbottom, pain, slow - 1 indicates a yes, 0 indicates no.
    # For passable, if "passable" is set, passleft, passtop, passright and passbottom are ignored.
    # If no data is specified for a tile that has a graphic, the tile is assumed to be non-passable only.
    # If a tile has no graphic, but has a value of -2, then its flags are evaluated, but nothing is drawn.
    mapdata = None

    # A list containing each of the characters on the map
    charlist = None

    # The player character
    playerchar = None

    # A list containing filename data about each char on the map
    chardata = None

    # A list of player fired projectiles on screen
    playerprojectiles = None

    # A list of npc fired projectiles on screen
    npcprocjectiles = None


    #def __init__(self, filename, viewrect=[ 0, 0, 0, 0, ]):
    def __init__(self):
        pass

    def load(self, filename, viewrect=[ 0, 0, 0, 0, ]):
        """Initialize the map object. Must specify filename and viewrect=(x, y, x2, y2) for it to work properly."""

        self.charlist = []

        # Define the starting position/total size of the viewport
        self.viewport = viewrect

        # Get the total width and height of the viewport
        self.viewportwidth = viewrect[2] - viewrect[0]
        self.viewportheight = viewrect[3] - viewrect[1]

        if filename:
            self.loaddata(filename)
        else:
            print "Error, no filename specified for __init__!"

        # Now get viewport width/height in tiles rather
        # Add one extra for offset

        # Debug
        #print self.viewportwidth, self.tilewidth, filename
        self.viewporttilewidth = self.viewportwidth / self.tilewidth + 1
        self.viewporttileheight = self.viewportheight / self.tileheight + 1

        # Set offset that needs to be added to the tile range each row for draw and coldet routines
        self.mapoffset = self.tileswide - self.viewporttilewidth

        # Debug
        #print self.playerchar.screenrect
        #print self.tileswide, self.tileshigh, self.viewporttilewidth, self.viewporttileheight, self.viewport

    def loadtiles(self, filename, borderwidth=1):
        """Load tilemap data"""

        self.maptiles = myimg.loadtiles(filename, tilewidth=self.tilewidth, tileheight=self.tileheight, border=borderwidth, colorkey=0)

    def loadbackground(self, filename, layer):
        """Load a background image to be displayed behind map"""

        # Note: Layers are drawn in order, back to front, 0, 1, 2
        # And because 2 is in back, it should be smaller than 1, and likewise with 1 and 0

        # Load an image for the first layer
        if layer == 0:
            self.bgimg0 = myimg.loadimage(filename)

            self.bgwidth0 = self.bgimg0.get_width()
            self.bgheight0 = self.bgimg0.get_height()

            # Figure out movement ratios
            self.bgratio0x = self.width / (self.bgwidth0  - self.viewportwidth)
            self.bgratio0y = self.height / (self.bgheight0  - self.viewportheight)

        # Load image for second layer
        elif layer == 1:
            self.bgimg1 = myimg.loadimage(filename, colorkey=-1)

            self.bgwidth1 = self.bgimg1.get_width()
            self.bgheight1 = self.bgimg1.get_height()

            # Figure out movement ratios
            self.bgratio1x = self.width / (self.bgwidth1 - self.viewportwidth)
            self.bgratio1y = self.height / (self.bgheight1 - self.viewportheight)

        # Load image for third layer
        elif layer == 2:
            self.bgimg2 = myimg.loadimage(filename, colorkey=-1)

            self.bgwidth2 = self.bgimg2.get_width()
            self.bgheight2 = self.bgimg2.get_height()

            # Figure out movement ratios
            self.bgratio2x = self.width / (self.bgwidth2 - self.viewportwidth)
            self.bgratio2y = self.height / (self.bgheight2 - self.viewportheight)


    def loaddata(self, filename=None):
        """Load the map data from given file."""

        if not filename:
            if self.mapfile:
                filename = self.mapfile
            else:
                print "No filename in function loaddata"
                return None
        else:
            self.mapfile = filename

        # Setup os independant path to file
        fullname = os.path.join(config.datadir, filename)

        try:
            # Open file for read
            f = file(fullname, 'r')
        except:
            print "Error opening file " + `filename` + " for load."
            return None

        # Now uncPickle the data
        # The name of the bitmap to load for tiles
        # The map tile flags list
        # The map tile list
        # The character data list
        # The map to load at level end
        # The background image to use
        self.width = cPickle.load(f)
        self.height = cPickle.load(f)
        self.tilewidth = cPickle.load(f)
        self.tileheight = cPickle.load(f)
        self.tilefile = cPickle.load(f)
        self.maplist = cPickle.load(f)
        self.mapdata = cPickle.load(f)
        self.chardata = cPickle.load(f)
        self.nextmaplist = cPickle.load(f)
        self.bgtag = cPickle.load(f)
        self.musicfile = cPickle.load(f)

        # Debug
        #print "Tile width and height", self.tilewidth, self.tileheight

        f.close()

        # Record total number of tiles in a map row/col
        self.tileswide = self.width / self.tilewidth
        self.tileshigh = self.height / self.tileheight

        # Set offset that needs to be added to the tile range each row for draw and coldet routines
        self.mapoffset = self.tileswide - self.viewporttilewidth

        # Now load the tiles from the given bitmap file
        self.loadtiles(self.tilefile)

        # Set our counter to 0
        counter = 0

        # Now load all of the chars
        for (charfile, x, y) in self.chardata:
            # The first char is going to be the main char
            if counter == 0:
                self.playerchar = game_entities.player( charfile, (x, y) )
                # Load a projectile set for the player
                self.playerprojectiles = game_projectiles.bullet_group("bullet1.bmp")
                # Add a reference to the projectile set for the player
                self.playerchar.bulletgroup = self.playerprojectiles
                counter += 1
            else:
                self.charlist += [ game_entities.char( charfile, (x, y) ) ]

        # Give the main char an ID this is legacy
        self.playerchar.id = 0

        # Create a main list of projectiles for npc's
        self.npcprojectiles = game_projectiles.bullet_group("bullet2.bmp")

        # Cause all other game chars to start moving
        for char in self.charlist:
            char.left_on()
            # Add a reference to the npc bullet group for npc's
            char.bulletgroup = self.npcprojectiles

        # Compile image names for background
        self.bgimg0 = self.bgtag + '0' + self.bgext
        self.bgimg1 = self.bgtag + '1' + self.bgext
        self.bgimg2 = self.bgtag + '2' + self.bgext

        # Debug
        #print self.bgimg0, self.bgimg1, self.bgimg2
        
        # Load backgrounds, (imgname, level)
        #self.bgimg0 = "default.background0.bmp"
        self.loadbackground(self.bgimg0, 0)
        self.loadbackground(self.bgimg1, 1)
        self.loadbackground(self.bgimg2, 2)

        if not self.maplist:
            self.status = "Error loading " + `filename` + "."
            return None
        else:
            self.status = "Map " + `filename` + " loaded."
            return 1

    def makelist(self):
        """Make a list of tiles to use in draw, and coldet routines.
        This function checks the last rangestart (first tile in the list of tiles,
        and if it is the same as last time, skips the rest of the processing.
        Note: also elemenates tiles with a maplist value of -1"""

        # Rangestart equals the viewportx distance from the left edge of the map
        # divided by the width of the tiles, plus the viewporty distance from top edge
        # of the map divided by the height of the tiles, multiplied by the number of tiles
        # in a row (A full row the length of the map). The rangestop is exactly the same
        # Except that we use viewportx + screenwidth, and viewporty + screenheight.
        # This tells us what tile we are starting at, and can be used in our range loop

        rangestart = self.viewport[0] / self.tilewidth
        rangestart += (self.viewport[1] / self.tileheight) * self.tileswide

        # If rangestart is different than last cycle, or there isn't any list yet
        # Note: This doesn't mean the map might not have moved,
        # only that it didn't move enough to cause new tiles to
        # to be processed.
        if rangestart != self.lastrangestart or not self.currentlist:
            # Copy rangestart for next cycle
            self.lastrangestart = rangestart

            # Clear the list
            self.currentlist = []

            # Screen coordinates
            x = 0
            y = 0

            # Set first tile
            n = rangestart

            listlen = len(self.maplist)

            # For each row
            for i in range(self.viewporttileheight):
                # Set y display position
                y = i * self.tileheight

                # For each column in row
                for j in range(self.viewporttilewidth):
                    # Set x display position
                    x = j * self.tilewidth

                    if n < listlen:
                        listid = self.maplist[n]
                    else:
                        listid = -1

                    if not listid < 0:
                        # Store tile number and x/y position of said tile
                        self.currentlist += [[n, x, y]]

                    n += 1

                # Reset x position
                x = 0

                # Increment to right position in next line
                n += self.mapoffset

    def draw(self, screen):
        """Draw the map to the screen"""


        # This turns off parallax scrolling
        if config.noparallax == 0:
            # Draw first background layer
            if self.bgratio0x > 1:
                xloc = self.viewport[0] / self.bgratio0x
            else:
                xloc = 0

            if self.bgratio0y > 1:
                yloc = self.viewport[1] / self.bgratio0y
            else:
                yloc = 0

            screen.blit(self.bgimg0, (0,0), (xloc, yloc, xloc + self.bgwidth0, yloc + self.bgheight0))
            
            # Draw second background layer
            if self.bgratio1x > 1:
                xloc = self.viewport[0] / self.bgratio1x
            else:
                xloc = 0

            if self.bgratio1y > 1:
                yloc = self.viewport[1] / self.bgratio1y
            else:
                yloc = 0

            screen.blit(self.bgimg1, (0, 0), (xloc, yloc, xloc +self.bgwidth1, yloc + self.bgheight1))

            # Draw third background layer
            if self.bgratio2x > 1:
                xloc = self.viewport[0] / self.bgratio2x
            else:
                xloc = 0

            if self.bgratio2y > 1:
                yloc = self.viewport[1] / self.bgratio2y
            else:
                yloc = 0

            screen.blit(self.bgimg2, (0,0), (xloc, yloc, xloc + self.bgwidth2, yloc + self.bgheight2))
        else:
            screen.blit(self.bgimg0, (0,0))

        # Find the current viewport offset (In pixels)
        xoff = self.viewport[0] % self.tilewidth
        yoff = self.viewport[1] % self.tileheight

        # For loop over (tileid, xpos, ypos) for each tile to display
        for (n,x,y) in (self.currentlist):
            listid = self.maplist[n]

            # Function make list should eleminate all tiles that are -1, but if not...
            #if (listid != -1):
            screen.blit(self.maptiles[ listid ], (x - xoff, y - yoff))

        # Draw the player's projectiles
        if self.playerprojectiles.bulletlist:
            # Draw the players projectiles
            self.playerprojectiles.draw(screen)

        # Draw the NPC's projectiles
        if self.npcprojectiles.bulletlist:
            # Draw the npcs projectiles
            self.npcprojectiles.draw(screen)
 
        # Draw the player
        self.playerchar.draw(screen)

        # Draw the enemy chars
        for char in self.charlist:
            char.draw(screen)

        if self.coldebug:
            # Draw troubleshooting highlights
            if self.blueblist:
                for (x,y) in (self.blueblist):
                    screen.blit(self.maptiles[ 55 ], (x, y))
                self.blueblist = []

            if self.greenblist:
                for (x,y) in (self.greenblist):
                    screen.blit(self.maptiles[ 54 ], (x, y))
                self.greenblist = []

            if self.redblist:
                for (x,y) in (self.redblist):
                    screen.blit(self.maptiles[ 36 ], (x, y))
                self.redblist = []

    def isonscreen(self, object, pad=500):
        """Determine based upon objects map location, whether or not it is on screen"""

        # If the object is not less than right side, greater than left side,
        # Less than top, or greater than bottom
        # All plus padding
        if object.maprect[0] > (self.viewport[2] + pad):
            return
        elif object.maprect[1] > (self.viewport[3] + pad):
            return
        elif object.maprect[2] < (self.viewport[0] - pad):
            return
        elif object.maprect[3] < (self.viewport[1] - pad):
            return
        else:
            # If we get to this point, the object is onscreen
            return 1

    def coldetmap(self, object):
        """Find collisions in a rect of object dimensions + (tilesize * 2) around object"""

        # Is the object is on screen
        onscreen = self.isonscreen(object)

        # If the object is not onscreen, set it's offscreen flag, this will prevent it from
        # updating until it is onscreen again.
        if onscreen == None:
            object.offscreen = 1
        elif onscreen == 1:
            object.offscreen = 0

            # If object is not a nonfalling/nonjumping item
            if object.id != 9:
                if object.jumping == 0:
                    object.falling = 1

            padwidth = self.tilewidth * 2
            padheight = self.tileheight * 2

            # Create list of tiles to check for collisions
            colrect = [ object.boundrect[0] - padwidth,
                        object.boundrect[1] - padheight,
                        object.boundrect[2] + padwidth,
                        object.boundrect[3] + padheight]

            # Setup the first tile to use in our test
            rangestart = colrect[0] / self.tilewidth
            rangestart += (colrect[1] / self.tileheight) * self.tileswide

            # Get collision rect width and height
            colrectwidth = colrect[2] - colrect[0]
            colrectheight = colrect[3] - colrect[1]

            # Get collision rect width and height in tiles
            colrecttilewidth = colrectwidth / self.tilewidth
            colrecttileheight = colrectheight / self.tileheight

            # Setup local offset for rows
            offset = self.tileswide - colrecttilewidth

            # The pixel offset of tiles
            xoff = colrect[0] % self.tilewidth
            yoff = colrect[1] % self.tileheight

            n = rangestart

            listlen = len(self.maplist)

            # Base tile flags
            tiledata = None

            # Loop through all columns
            for i in range(colrecttileheight):
                y = colrect[1]
                y += i * self.tileheight
                y -= yoff

                # Loop through all rows
                for j in range(colrecttilewidth):
                    x = colrect[0]
                    x += j * self.tilewidth
                    x -= xoff

                    if self.coldebug:
                        self.blueblist += [(x - self.viewport[0], y - self.viewport[1])]

                    if n < listlen:
                        listid = self.maplist[n]

                        # Get flags (passable etc...) about this tile if they exist
                        if self.mapdata[n]:
                            tiledata = self.mapdata[n]
                    else:
                        listid = -1

                    # Note, the following method of tracking whether or not we have the
                    # left or right /lower-most tile only works when our test field is
                    # wider than two tiles width. I can't think of a reason why it would ever
                    # be less than two, but it's worth noting.

                    # If listid is not blank (-1) (-2 is non-drawing, but is evaluated for collisions)
                    if listid != -1 and n > 0:
                        # Set x/y bounding location

                        ## Temporary, construct a rect for this tile
                        ## to use with collision detection.
                        ## Later get this statically.
                        rect = (x, y, x + self.tilewidth, y + self.tileheight)

                        if self.coldebug:
                            # Add this tile to the blockout list
                            self.redblist += [(x - self.viewport[0], y - self.viewport[1])]

                        # Check for collisions with object and map tiles
                        if object.boundrect[3] < rect[1]:
                            pass
                        elif object.boundrect[1] > rect[3]:
                            pass
                        elif object.boundrect[2] < rect[0]:
                            pass
                        elif object.boundrect[0] > rect[2]:
                            pass
                        else:
                            # Get overlaping rect
                            # Psuedo: if ax1 < bx1: cx1 = bx1, cx2 = ax2; else: cx1 = ax1, cx2 = bx2
                            # Psuedo: if ay1 < by1: cy1 = by1, cy2 = ay2; else: cy1 = ay1, cy2 = by2

                            # New rect for collision rect
                            overlaprect = [ 0, 0, 0, 0 ]

                            # Flags to record whether or not the collison was at the top or left,
                            # i.e. if not, it was on the bottom or right respectively.
                            top = 0
                            left = 0

                            # Actual get overlaping rect
                            if object.boundrect[0] < rect[0]:
                                # Collision happend on the right side
                                left = 0
                                overlaprect[0] = rect[0]
                                overlaprect[2] = object.boundrect[2]
                            elif object.boundrect[0] >= rect[0]:
                                # Collision happend on the left side
                                left = 1
                                overlaprect[0] = object.boundrect[0]
                                overlaprect[2] = rect[2]

                            if object.boundrect[1] < rect[1]:
                                # Collision happened at the bottom of the object
                                top = 0
                                overlaprect[1] = rect[1]
                                overlaprect[3] = object.boundrect[3]
                            elif object.boundrect[1] >= rect[1]:
                                # Collision happened at the top of the object
                                top = 1
                                overlaprect[1] = object.boundrect[1]
                                overlaprect[3] = rect[3]

                            if listid == -3 and object.id == 0:

                                # We need to check if there are any bosses on the level, and if there are, 
                                # they must be dead before we exit the level
                                endokay = 1
                                for npc in self.charlist:
                                    if npc.boss > 0:
                                        if npc.dead == 1 or npc.dying == 1:
                                            break
                                        else:
                                            endokay = 0
                                            break

                                if endokay == 1:
                                    # Player made it to the end of the level, set state to endlvl
                                    self.state = 3
                                    # Get the name of the next map to load
                                    self.nextmap = self.nextmaplist[n]

                                # Debug
                                #print "END LEVEL", self.nextmap

                            if listid == -4 and object.id != 0 and object.id != 9:
                                # If not player, but had collision with "Invisible NPC block" 

                                # Now pass collision data to object for it to resolve
                                object.ifcollide(overlaprect, tiledata, top, left)

                            elif listid != -4:
                                # Now pass collision data to object for it to resolve
                                object.ifcollide(overlaprect, tiledata, top, left)

                    n += 1

                x = 0

                n += offset

    def charcol(self, object):
        """Run collision tests for object against all onscreen chars"""

        # Note: This function probably needs to be cleaned up, and setup such that only the main char is tested

        # I need to think about this... Do we really need collision detection between enemies? I'm thinking at the moment
        # that we really only need detection between the main char and the enemies...

        # For each char in the charlist
        for char in self.charlist:
            # Then if this char is not offscreen
            if not char.offscreen and not char.dead:
                # Do collision tests, if pass all, collision occured
                if char.boundrect[2] < object.boundrect[0]:
                    pass
                elif char.boundrect[3] < object.boundrect[1]:
                    pass
                elif char.boundrect[0] > object.boundrect[2]:
                    pass
                elif char.boundrect[1] > object.boundrect[3]:
                    pass
                else:
                    # New rect for collision rect
                    overlaprect = [ 0, 0, 0, 0 ]

                    # Flags to record whether or not the collison was at the top or left,
                    # i.e. if not, it was on the bottom or right respectively.
                    top = 0
                    left = 0

                    # Actual get overlaping rect
                    if object.boundrect[0] < char.boundrect[0]:
                        # Collision happend on the right side
                        left = 0
                        overlaprect[0] = char.boundrect[0]
                        overlaprect[2] = object.boundrect[2]
                    elif object.boundrect[0] >= char.boundrect[0]:
                        # Collision happend on the left side
                        left = 1
                        overlaprect[0] = object.boundrect[0]
                        overlaprect[2] = char.boundrect[2]

                    if object.boundrect[1] < char.boundrect[1]:
                        # Collision happened at the bottom of the object
                        top = 0
                        overlaprect[1] = char.boundrect[1]
                        overlaprect[3] = object.boundrect[3]
                    elif object.boundrect[1] >= char.boundrect[1]:
                        # Collision happened at the top of the object
                        top = 1
                        overlaprect[1] = object.boundrect[1]
                        overlaprect[3] = char.boundrect[3]

                    # Okay now what we need to know is, did the main char land on the enemy?
                    # And if so, did the char get damaged, or did the enemy or both?

                    # Alright, I should probably write a new char only collision routine for the char object.
                    # Should probably actually take the main char and seperate it out into another derivation of the
                    # base char object.

                    # Do collision for both main char, and other char
                    object.ifcollidechar(overlaprect, char, top, left)

    def playercharcol(self, object):
        """Check for a collision with main character"""

        # Do collision tests, if pass all, collision occured
        if self.playerchar.boundrect[2] < object.boundrect[0]:
            pass
        elif self.playerchar.boundrect[3] < object.boundrect[1]:
            pass
        elif self.playerchar.boundrect[0] > object.boundrect[2]:
            pass
        elif self.playerchar.boundrect[1] > object.boundrect[3]:
            pass
        else:
            # New rect for collision rect
            overlaprect = [ 0, 0, 0, 0 ]

            # Flags to record whether or not the collison was at the top or left,
            # i.e. if not, it was on the bottom or right respectively.
            top = 0
            left = 0

            # Actual get overlaping rect
            if object.boundrect[0] < self.playerchar.boundrect[0]:
                # Collision happend on the right side
                left = 0
                overlaprect[0] = self.playerchar.boundrect[0]
                overlaprect[2] = object.boundrect[2]
            elif object.boundrect[0] >= self.playerchar.boundrect[0]:
                # Collision happend on the left side
                left = 1
                overlaprect[0] = object.boundrect[0]
                overlaprect[2] = self.playerchar.boundrect[2]

            if object.boundrect[1] < self.playerchar.boundrect[1]:
                # Collision happened at the bottom of the object
                top = 0
                overlaprect[1] = self.playerchar.boundrect[1]
                overlaprect[3] = object.boundrect[3]
            elif object.boundrect[1] >= self.playerchar.boundrect[1]:
                # Collision happened at the top of the object
                top = 1
                overlaprect[1] = object.boundrect[1]
                overlaprect[3] = self.playerchar.boundrect[3]

            # Do collision for main char
            self.playerchar.ifcollidechar(overlaprect, object, top, left)

    def mapedgecol(self, object):
        """Detect and respond to a collision between an object and the viewport edge"""

        # If object x is less than 0
        if object.maprect[0] < 0:
            # Construct a rect for collision
            rect = [ object.maprect[0], object.maprect[1], 0, object.maprect[3] ]

            # Call collison routine, we are telling ifcollide that collision happend on left/bottom of object.
            object.ifcollide(rect, None, 0, 1)

        # If object is greater than total map width
        elif object.maprect[2] > self.width:
            # Construct a rect for collision
            rect = [ self.width, object.maprect[1], object.maprect[2], object.maprect[3] ]

            # Call collison routine, we are telling ifcollide that collision happend on right/bottom of object.
            object.ifcollide(rect, None, 0, 0)

        # If object y is less than 0
        if object.maprect[1] < 0:
            # Construct a rect for collision
            rect = [ object.maprect[0], object.maprect[1], object.maprect[2], 0 ]

            # Call collison routine, we are telling ifcollide that collision happend on left/top of object.
            object.ifcollide(rect, None, 1, 1)

        # If object y is greater than map height
        elif object.maprect[3] > self.height:
            # Construct a rect for collision
            rect = [ object.maprect[0], self.height, object.maprect[2], object.maprect[3] ]

            # Call collison routine, we are telling ifcollide that collision happend on left/bottom of object.
            object.ifcollide(rect, None, 0, 1)

    def hardcenter(self, object, noadjust=0):
        """Center the map on a target"""

        self.xlast = self.viewport[0]
        self.ylast = self.viewport[1]

        copyrect = object.screenrect[:]

        # Find where the map should be to be properly centered on this item
        xgoal = (self.viewportwidth - object.width) / 2
        ygoal = (self.viewportheight - object.height) / 2

        # The ammount to move by
        move = 0

        # Test for object out of the x/ygoal bounds
        # Test for screen bounds, and if all workout, move screen and object by appropriate opposing ammounts.
        if copyrect[0] > xgoal:
            move = copyrect[0] - xgoal
            if not ((self.viewport[2] + move) >= (self.width - 1)):
                self.adjustpos(x=move)

                if not noadjust:
                    self.playerchar.adjposscr(x=-move)

                # Adjust the position of all of the other chars in the map.
                for object in self.charlist:
                    object.adjposscr(x=-move)

                self.playerprojectiles.adjposscr(x=-move)
                self.npcprojectiles.adjposscr(x=-move)

                # Debug
                #print " > xgoal"

        elif copyrect[0] < xgoal:
            move = xgoal - copyrect[0]
            if not ((self.viewport[0] - move) <= 0):
                self.adjustpos(x=-move)

                if not noadjust:
                    self.playerchar.adjposscr(x=move)

                # Adjust the position of all of the other chars in the map.
                for object in self.charlist:
                    object.adjposscr(x=move)

                self.playerprojectiles.adjposscr(x=move)
                self.npcprojectiles.adjposscr(x=move)

                # Debug
                #print " < xgoal"

        if copyrect[1] > ygoal:
            move = copyrect[1] - ygoal
            if not ((self.viewport[3] + move) >= self.height):
                self.adjustpos(y=move)

                if not noadjust:
                    # Adjust the players position
                    self.playerchar.adjposscr(y=-move)

                # Adjust the position of all of the other chars in the map.
                for object in self.charlist:
                    object.adjposscr(y=-move)

                self.playerprojectiles.adjposscr(y=-move)
                self.npcprojectiles.adjposscr(y=-move)

                # Debug
                #print " > ygoal"

            else:
                leftovermove = self.height - self.viewport[3]
                self.adjustpos(y=leftovermove)

                if not noadjust:
                    # Adjust the players position
                    self.playerchar.adjposscr(y=-leftovermove)

                # Adjust the position of all of the other chars in the map.
                for object in self.charlist:
                    object.adjposscr(y=-leftovermove)

                self.playerprojectiles.adjposscr(y=-leftovermove)
                self.npcprojectiles.adjposscr(y=-leftovermove)

                # Debug
                #print " > ygoal leftover"

        elif copyrect[1] < ygoal:
            move = ygoal - copyrect[1]
            if not ((self.viewport[1] - move) <= 0):
                self.adjustpos(y=-move)

                if not noadjust:
                    # Adjust the players position
                    self.playerchar.adjposscr(y=move)

                # Adjust the position of all of the other chars in the map.
                for object in self.charlist:
                    object.adjposscr(y=move)

                self.playerprojectiles.adjposscr(y=move)
                self.npcprojectiles.adjposscr(y=move)

                # Debug
                #print " < ygoal"

    def softcenter(self, object, speed=15):
        """Center map on target via soft updates, i.e. add velocity until the map reaches a certain point"""

        #print "DO NOT USE THIS FUNCTION UNTIL BULLET POSITION ADJUSTMENT IS FIXED"

        # Padding for centering
        pad = 2 * speed

        # Find where the map should be to be properly centered on this item
        xgoal = (self.viewportwidth - object.width) / 2
        ygoal = (self.viewportheight - object.height) / 2

        #print xgoal, ygoal, object.screenrect

        # If the object is position is greater than goal + plus a little padding
        # elif the object is position is less than goal
        if object.screenrect[0] > xgoal + pad:
            # Is viewport already at max?
            if not (self.viewport[2] >= (self.width - 1)):
                # Debug
                #print self.viewport[2], self.width, "right"

                # Will viewport be greater than max at next cycle?
                if (self.viewport[2] + speed) > (self.width - 1):
                    speed = self.width - self.viewport[2] - 1

                self.xvel += speed

                # Adjust the players position
                self.playerchar.adjposscr(x=-speed)

                for char in self.charlist:
                    char.adjposscr(x=-speed)

        elif object.screenrect[0] < xgoal - pad:
            if not (self.viewport[0] <= 0):
                #Debug
                #print self.viewport[0], 0, "left"

                if (self.viewport[0] - speed) < 0:
                    speed = self.viewport[0]

                self.xvel -= speed

                # Adjust the players position
                self.playerchar.adjposscr(x=speed)

                for char in self.charlist:
                    char.adjposscr(x=speed)

        if object.screenrect[1] > ygoal + pad:
            if not (self.viewport[3] >= self.height - 1):
                #Debug
                #print self.viewport[3], self.height, "down"

                if (self.viewport[3] + speed) > (self.height - 1):
                    speed = self.height - self.viewport[3] - 1

                self.yvel += speed

                # Adjust the players position
                self.playerchar.adjposscr(y=-speed)

                for char in self.charlist:
                    char.adjposscr(y=-speed)

        elif object.screenrect[1] < ygoal - pad:
            if not (self.viewport[1] <= 0):
                #Debug
                #print self.viewport[1], 0, "up"

                if (self.viewport[1] - speed) < 0:
                    speed = self.viewport[1]

                self.yvel -= speed

                # Adjust the players position
                self.playerchar.adjposscr(y=speed)

                for char in self.charlist:
                    char.adjposscr(y=speed)

    def returnmessage(self):
        """Send a state message, level end, player died, etc..."""

        # Record state for send
        message = self.state

        # Reset state
        self.state = 0

        return(message)

    def sync(self):
        """Run all updates and sync map"""

        # If the main char is dead, we need to re-set it, the map, and all of the other chars
        if self.playerchar.dead:
            # Player died, set state to 1
            #self.state = 1

            # Reset player
            self.playerchar.reset()

            # Reset enemies
            for char in self.charlist:
                char.reset()

            # Reset map
            self.viewport = [0, 0, self.viewportwidth, self.viewportheight]
            return

        # If there are any player bullets
        if self.playerprojectiles.bulletlist:
            # for each bullet in the list
            for bullet in self.playerprojectiles.bulletlist:
                # If the bullet is on screen, with zero pad
                if self.isonscreen(bullet, pad=0) and bullet.hitpoints > 0:
                    bullet.update()
                    self.coldetmap(bullet)
                    self.charcol(bullet)
                else:
                    # Else delete the bullet
                    self.playerprojectiles.delete(bullet)

        # If there are any npc bullets
        if self.npcprojectiles.bulletlist:
            # for each bullet in the list
            for bullet in self.npcprojectiles.bulletlist:
                # If the bullet is on screen, with zero pad
                if self.isonscreen(bullet, pad=0) and bullet.hitpoints > 0:
                    bullet.update()
                    self.coldetmap(bullet)
                    self.playercharcol(bullet)
                else:
                    # Else delete the bullet
                    self.npcprojectiles.delete(bullet)

        # Update each char in the list
        for char in self.charlist:

            if self.isonscreen(char):

                # Run char updates
                char.update()

                # Only do ai and collisions if the char isn't dead.
                if not char.dead and not char.inanimate == 1:

                    # Run AI
                    char.ai(self.playerchar)

                    # Run collision detection tests with this char against the map
                    self.coldetmap(char)

                    self.mapedgecol(char)

        # Debug
        #print self.playerchar.screenrect, self.playerchar.maprect, self.playerchar.boundrect

        # Update player
        self.playerchar.update()

        # Center on main char
        self.hardcenter(self.playerchar)

        # Run player collision tests
        self.coldetmap(self.playerchar)

        # run colision detection for all chars onscreen
        self.charcol(self.playerchar)

        # Update map
        self.update()

        # Check for collisions with edge of map and main char
        self.mapedgecol(self.playerchar)

    def update(self):
        """Update viewport position and record last upper left postion"""


        self.viewport[0] += self.xvel
        self.viewport[1] += self.yvel
        self.viewport[2] += self.xvel
        self.viewport[3] += self.yvel

        self.xvel = 0
        self.yvel = 0

        if self.viewport[0] < 0:
            self.move(x=-1)
        if self.viewport[1] < 0:
            self.move(y=-1)
        if self.viewport[2] > self.width:
            self.move(x=self.width - 1)
        if self.viewport[3] > self.height:
            self.move(y=self.height - 1)

    def adjustpos(self, x=None, y=None):
        """Adjust position by x or y"""

        if x != None:
            self.viewport[0] += x
            self.viewport[2] += x

        if y != None:
            self.viewport[1] += y
            self.viewport[3] += y

    def move(self, x=None, y=None):
        """Move map to x/y position, if x or y == -1 revert to last position"""

        if x == -1:
            self.viewport[0] = self.xlast
            self.viewport[2] = self.viewport[0] + self.viewportwidth
        elif x:
            self.viewport[0] = x
            self.viewport[2] = self.viewport[0] + self.viewportwidth

        if y == -1:
            self.viewport[1] = self.ylast
            self.viewport[3] = self.viewport[1] + self.viewportheight
        elif y:
            self.viewport[1] = y
            self.viewport[3] = self.viewport[1] + self.viewportheight

    def save(self, filename):
        """The purpose of this function is to save the state of the current map"""

        # Save the player and enemy state
        pass
