Exemple #1
0
 def enableRainEffect(self):
     """Enable the rain effects in this level"""
     self._rain = Rain(self.levelSize)
Exemple #2
0
class GameLevel(GridMapSupport):
    """This is the class of a game level.
    Character move inside a level using some of its methods.
    
    When the level is draw, after the background image, all groups stored in this level
    are drawn ordered by zindex info.
    
    A GameLevel object can store a lot of groups; those groups (of tipe GameGroup) can be updatable or not.
    When a level is updated - calling the update(time_passed) method - on all groups of updatable type is
    called the update method.
    """
    
    def __init__(self, name, size, background=""):
        """Init a level object with a name and a size.
        If a background file name is given, this image is loaded as background.
        If you don't give a background then the level name (converted in a lowecase, less separated png file name)
        is used instead.
        If you really don't have a level image, please use a background parameter to None
        """
        GridMapSupport.__init__(self)
        self.name = name
        self.levelSize = size
        if background == '':
            background = name.lower().replace(" ","-")+".png"
        if background:
            self._background = utils.load_image(background, directory="levels")
        else:
            self._background = None
        self._topleft = (0,0)
        self._centeringSpeed = 50
        self._centeringHeading = None
        self._groups = []
        self._groups_toupdate = []
        self._groups_todraw = []
        self._rain = None
        self.screenReferenceSprite = None
        self.hero = None
        self.timeIn = 0.
        self.shadow = True
        
        # The presentation object running
        self.presentation = None
        self.level_text = None
        
        # This index will act on the ability of characters to hide in shadows in the level
        self.stealthLevel = 1.
        
        # Groups!
        all = GameGroup("all")
        dead = GameGroup("dead", drawable=True, updatable=True)
        physical = GameGroup("physical", drawable=True, updatable=True)
        # visual_obstacles are sprites that block character sight
        visual_obstacles = GameGroup("visual_obstacles")
        charas = GameGroup("charas")
        enemies = GameGroup("enemies")
        animations = GameGroup("animations", drawable=True, updatable=True)
        top_animations = GameGroup("top_animations", drawable=True, updatable=True)
        speech = GameGroup("speech", updatable=True)                                # speechs are handled in a special way
        level_text = GameGroup("level_text")
        exits = GameGroup("exits", drawable=True, updatable=True)
        triggers = GameGroup("triggers", drawable=True, updatable=True)
        tippable = GameGroup("tippable")                                            # Tips are handled in a special way
        self.addGroup(all)
        self.addGroup(dead, zindex=5)
        self.addGroup(physical, zindex=10)
        self.addGroup(visual_obstacles)
        self.addGroup(charas, zindex=10)
        self.addGroup(enemies)
        self.addGroup(animations, zindex=3)
        self.addGroup(top_animations, zindex=20)
        self.addGroup(speech)
        self.addGroup(level_text, zindex=30)
        self.addGroup(exits, zindex=3)
        self.addGroup(triggers, zindex=4)
        self.addGroup(tippable)

    def __getitem__(self, key):
        """Get a group by its name, or look for a GameSprite with that name if no group is found.
        """
        for group in self._groups:
            if group[1].name==key:
                return group[1]
        # not found a group; check for a GameSprite
        for group in self._groups:
            for sprite in group[1].sprites():
                if sprite.name==key:
                    return sprite
        raise KeyError("Not found group or sprite named '%s'" % key)

    def _setTopLeft(self, topleft):
        self._topleft = self._normalizeDrawPart(topleft)
    topleft = property(lambda self: self._topleft, _setTopLeft, doc="""The topleft drawing point inside the level background image""")
    
    @property
    def midbottom(self):
        """Midbottom of the screen on the level"""
        return self.rect.midbottom

    @property
    def rect(self):
        """A pygame.Rect from the level topleft, with the screen size"""
        return pygame.Rect(self.topleft, cblocals.GAME_SCREEN_SIZE)

    @property
    def center(self):
        """The point at the center of the drawn level part"""
        return self.rect.center

    def addGroup(self, group, zindex=10):
        """Add a new group the this level.
        All group are added to a default zindex info of 10.
        """
        self._groups.append( (zindex,group) )
        self._groups.sort(lambda x,y: x[0]-y[0])
        if group.updatable:
            self._groups_toupdate.append( (zindex,group) )
            self._groups_toupdate.sort(lambda x,y: x[0]-y[0])
        if group.drawable:
            self._groups_todraw.append( (zindex,group) )
            self._groups_todraw.sort(lambda x,y: x[0]-y[0])     
    
    def generateRandomPoint(self, fromPoint=(), maxdistance=0):
        """Generate a random point on the level.
        You can use this giving a distance and a start point to get a random point near that position.
        Normally the point is taken at random on level size.
        """
        if fromPoint and maxdistance:
            offset_x = cbrandom.randint(-maxdistance,maxdistance)
            offset_y = cbrandom.randint(-maxdistance,maxdistance)
            startX = fromPoint[0] - maxdistance
            endX = fromPoint[0] + maxdistance
            startY = fromPoint[1] - maxdistance
            endY = fromPoint[1] + maxdistance
        else:
            # If one of the not required param is missing, always use the normal feature
            startX = 1
            endX = self.levelSize[0]-1
            startY = 1
            endY = self.levelSize[1]-1
        return self.normalizePointOnLevel(cbrandom.randint(startX,endX), cbrandom.randint(startY,endY))

    def normalizePointOnLevel(self, x, y):
        """Given and x,y pair, normalize this to make this point valid on level coordinates"""
        if x<1: x=1
        elif x>self.levelSize[0]-1: x=self.levelSize[0]-1
        if y<1: y=1
        elif y>self.levelSize[1]-1: y=self.levelSize[1]-1 
        return x,y

    def normalizeRectOnScreen(self, rect):
        """Given a rect, make this rect is always contained in the screen"""
        w,h = cblocals.GAME_SCREEN_SIZE
        if rect.left<0:
            rect.left = 0
        if rect.right>w:
            rect.right = w
        if rect.top<0:
            rect.top = 0
        if rect.bottom>h:
            rect.bottom = h
        return rect

    def checkPointIsValidOnLevel(self, point, screenCoordinate=False):
        """Check if a coordinate is valid in current level.
        If screenCoordinate is True, then x,y will be normalized to level coord before use
        (calling transformToLevelCoordinate).
        """
        if screenCoordinate:
            x,y = self.transformToLevelCoordinate(point)
        if x<1: return False
        elif x>self.levelSize[0]-1: return False
        if y<1: return False
        elif y>self.levelSize[1]-1: return False 
        return True

    def checkPointIsFree(self, point):
        """Check if a coordinate is related to a free point on the level"""
        physical = self['physical']
        point = self.transformToScreenCoordinate(point)
        for sprite in physical.sprites():
            if sprite.collide_rect.collidepoint(point):
                return False
        return True

    def checkRectIsInLevel(self, r):
        """Check if a rect is inside the level area"""
        levelRect = pygame.Rect( (0,0), self.levelSize)
        return levelRect.contains(r)

    def addSprite(self, sprite, firstPosition=None):
        """Add a sprite to this level at given position"""
        if not firstPosition:
            firstPosition = sprite.position
        sprite.addToGameLevel(self, firstPosition)
        # I memoize here sprites that use tips; tip can be evaluated False but not None
        if sprite.getTip() is not None:
            self['tippable'].add(sprite)

    def getCloserEnemy(self, character):
        """Return an enemy in the character sight range, or None"""
        sight = character.sightRange
        group = self['charas']
        enemies = []
        for charas in group.sprites():
            distanceFromCharas = character.distanceFrom(charas)
            if character.side!=charas.side and distanceFromCharas<=sight:
                # Check now if he can see the character
                if character.hasFreeSightOn(charas):
                    if charas.stealth:
                        # A rogue charas reduce the character sight range
                        if distanceFromCharas <= int(sight * charas.getPreySightRangeReductionFactor()):
                            # ... and now the real check for the anti-stealth
                            if character.check_antiStealth(charas):
                                enemies.append(charas)
                    else:    
                        enemies.append(charas)
        if enemies:
            return cbrandom.choice(enemies)
        return None

    def draw(self, screen):
        """Draw the level"""
        physical = self['physical']
        # 1 * Draw the background image
        if self._background:
            screen.blit(self._background.subsurface(pygame.Rect(self.topleft, cblocals.GAME_SCREEN_SIZE) ), (0,0) )
        # 2 * Draw all sprite groups
        for group in self._groups_todraw:
            # special handling for the physical group
            if group[1] is physical and not self.presentation:
                group[1].draw(screen, self.hero)
            else:
                group[1].draw(screen)
        if cblocals.DEBUG:
            # 3 * Draw screen center (if in DEBUG mode)
            xy, wh = self._getScreenCenter()
            xy = self.transformToScreenCoordinate(xy)
            pygame.draw.rect(screen, (50,250,250), (xy, wh), 1)
            # 4 * Draw gripmap squares
            self.drawGridMapSquares(screen)
        self.drawRain(screen)
        # Special use of the level_text group
        if self['level_text']:
            self['level_text'].draw(screen)

    def update(self, time_passed):
        """Call the group update method on all (updatable) groups stored in this level"""
        for group in self._groups_toupdate:
            group[1].update(time_passed)
        if self._rain:
            self._rain.update(time_passed)
        self.timeIn+=time_passed

    def update_text(self, time_passed):
        """An update method that only works with the 'level_text' group"""
        if len(self['level_text'])>0:
            self['level_text'].update(time_passed)
            return True
        return False

    def _normalizeDrawPart(self, topleft=None, size=None):
        """Called to be sure that the draw portion of level isn't out of the level total surface"""
        x,y = topleft or self._topleft
        w,h = size or self.levelSize
        sw, sh = cblocals.GAME_SCREEN_SIZE
        if x<0:
            x=0
        if y<0:
            y=0
        if x+sw>w:
            x = w-sw
        if y+sh>h:
            y = h-sh
        return x,y
        
    def hasBackground(self):
        """Check is this level has a background image"""
        return self._background is not None

    def generateDeadSprite(self, corpse):
        """Using a character (commonly... very commonly... a DEAD ones!), generate a corpse.
        Corpse is added to a group called "dead".
        """
        sprite = GameSprite()
        sprite.image = utils.getRandomImageFacingUp(corpse.images)
        curRect = corpse.rect
        newRect = pygame.Rect( curRect.topleft, sprite.image.get_rect().size )
        newRect.midbottom = corpse.position
        sprite.rect = newRect
        self["dead"].add(sprite)
        sprite.addToGameLevel(self, corpse.position)

    def normalizeDrawPositionBasedOn(self, reference, time_passed):
        """Slowly move drawn portion of the total level, for centering it on the given reference object.
        reference can be a GameSprite or a position tuple info.
        """
        if type(reference)==tuple:
            referencePointOnScreen = reference
        else:
            referencePointOnScreen = reference.position
        if self.isPointAtScreenCenter(referencePointOnScreen, (160,160) ):
            return
        heading = Vector2.from_points(self.center, referencePointOnScreen)
        # More near to screen border, faster will be the screen centering procedure
        speed = self._centeringSpeed
        magnitude = heading.get_magnitude()
        if magnitude<120:
            pass
        elif magnitude<150:
            speed = speed*3
        elif magnitude<200:
            speed = speed*5
        elif magnitude>=250:
            speed = speed*6                        
        
        heading.normalize()
        distance = time_passed * speed
        movement = heading * distance
        x = movement.get_x()
        y = movement.get_y()
        self.topleft = (self.topleft[0]+x,self.topleft[1]+y)

    def _getScreenCenter(self, centerSize=(200,200)):
        """Return the screen center rect as ( (x,y), (w,h) )
        w and h are taken by the optional centerSize param.
        """
        cx, cy = self.center
        return (cx-centerSize[0]/2,cy-centerSize[1]/2), centerSize

    def isPointAtScreenCenter(self, refpoint, centerSize=(200,200)):
        """This method return true if a given point is inside a rect of given size.
        The rect is placed at the screen center
        """
        cx, cy = self.center
        rect = pygame.Rect( self._getScreenCenter(centerSize) )
        return rect.collidepoint(refpoint)

    def transformToLevelCoordinate(self, position):
        """Given a screen coordinate, transform it to level absolute position"""
        x, y = position
        tx, ty = self.topleft
        return (int(tx+x), int(ty+y))
    
    def transformToScreenCoordinate(self, position):
        """Given an level coordinate, transform it to screen position"""
        x, y = position
        tx, ty = self.topleft
        return (x-tx, y-ty)

    def addPhysicalBackground(self, position, size, groups=['physical']):
        """Add a PhysicalBackground instance to the current level.
        Use groups param to add the sprite to some groups stored in the level also.
        See PhysicalBackground for more info.
        """
        pb = PhysicalBackground( position, size )
        for group_name in groups:
            group = self[group_name]
            group.add(pb)
            self.addSprite(pb, position)

    def addPhysicalSightBlockingBackground(self, position, size, groups=['physical','visual_obstacles']):
        """Like addPhysicalBackground but also add the sprite to the 'visual_obstacles' group"""
        self.addPhysicalBackground(position, size, groups)

    def addAnimation(self, position, animation, groups=['animations']):
        """Add an animation sprite to this level.
        If animation parameter is a string, the engine try to load an animation with that name.
        """
        if type(animation)==str:
            animation = utils.loadAnimationByName(animation, position)    
        for group_name in groups:
            group = self[group_name]
            group.add(animation)
            # some animation can change original position
            if animation.position:
                position = animation.position
            self.addSprite(animation, position)

    def addAnimations(self, positions, animation, groups=['animations']):
        """Service method for call addAnimation multiple times, for fast add the same animation in many
        places on the level.
        """
        for position in positions:
            self.addAnimation(position, animation, groups=groups)

    def enableRainEffect(self):
        """Enable the rain effects in this level"""
        self._rain = Rain(self.levelSize)

    def drawRain(self, surface):
        """Draw the rain effect on the surface passed.
        This call do something only if the level has inited the rain effect calling GameLevel.enableRainEffect()
        """
        if self._rain:
            self._rain.draw(surface)

    def enablePresentation(self, presentation, presentation_dir="data/presentations"):
        """Load a presentation using its name and return a Presentation object.
        Also store current level presentation
        """
        if not presentation.endswith(".cbp"):
            presentation+=".cbp"
        pp = Presentation(self, presentation, presentation_dir=presentation_dir)
        self.presentation = pp
        pp.enablePresentationMode()
        return pp

    
    # ******* Level text methods *******
    def _useLevelText(self, text, type=cblocals.LEVEL_TEXT_TYPE_NORMAL):
        level_text = self.level_text
        if not level_text:
            level_text = LevelText(self, text, type)
            level_text.addToGameLevel(self, level_text.position)
            self.level_text = level_text
        else:
            level_text.addText(text)

    def levelTextIntro(self, text, colophon_flag=False):
        self._useLevelText(text, type=cblocals.LEVEL_TEXT_TYPE_BLACKSCREEN)
        if colophon_flag:
            self.level_text.colophon = True

    def levelText(self, text):
        self._useLevelText(text)
    # ******* /Level text methods *******

    def displayTip(self, surface, tip_sprite):
        """Display a GameSprite's tip on the surface, and control the tip position"""
        tip_structure = tip_sprite.getTip()
        if not tip_structure:
            return
        text = tip_structure['text']
        color = tip_structure['color']
        font = tip_structure.get('font',cblocals.default_font)
        background = tip_structure.get('background',None)
        fw,fh = font.size(text)
        rect = tip_sprite.physical_rect
        x,y = rect.midtop
        x=x-fw/2
        y=y-fh
        font_rect = pygame.Rect( (x,y),(fw,fh) )
        # Draw sprites's tip only if sprite is inside the screen
        if self.rect.colliderect(rect.move(self.topleft)):
            font_rect = self.normalizeRectOnScreen(font_rect)
            tip = font.render(text, True, color)
            if background:
                pygame.draw.rect(surface, background, font_rect, 0)
            surface.blit(tip, font_rect.topleft)


    def blitShadow(self, surface, refSprite):
        """Blit game general shadow.
        If the shadow has a light area, its based on a reference sprite.
        Do nothing if GameLevel.shadow is False
        """
        if self.shadow:
            if not self.presentation:
                if not refSprite.outOfScreen:
                    spriteposx, spriteposy = self.transformToScreenCoordinate(refSprite.position)
                    imgsizew, imgsizeh = cblocals.SHADOW_IMG_SIZE
                    surface.blit(cblocals.shadow_image, (spriteposx-imgsizew/2, spriteposy-imgsizeh/2))
                else:
                    surface.blit(cblocals.total_shadow_image_09, (0,0) )
            else:
                surface.blit(cblocals.total_shadow_image_05, (0,0) )