Example #1
0
    def __init__(self, name, img, containers,
                 realSize=cblocals.TILE_IMAGE_SIZE, speed=150.,
                 attackTime=0.5, afterAttackRestTime=0.2, weaponInAndOut=False, sightRange=200,):
        
        GameSprite.__init__(self, *containers)
        Stealth.__init__(self)
        Warrior.__init__(self, attackTime, afterAttackRestTime)

        self._x = self._y = 0
        self.rect = pygame.Rect( (self.x, self.y), (cblocals.TILE_IMAGE_SIZE) )

        self.name = name
        self.characterType = "Guy"
        self.experienceLevel = 1
        
        self._brain = None
        self._presentationBrain = PresentationStateMachine(self)
        
        self._load_images(img, weaponInAndOut)
        self.lastUsedImage = 'head_south_1'

        self._stepTime = 0
        self._mustChangeImage = False
        self.direction = self._lastUsedDirection = cblocals.DIRECTION_S
        self._isMoving = False
        self.maxSpeed = self._speed = speed
        self.sightRange = sightRange
        self.rest_time_needed = .3
        
        self.side = 'Cheese Boys'
        self._enemyTarget = None
        
        self.navPoint = NavPoint(self)
        self.heading =  None
        
        # From where a succesfull attack is coming
        self.damageHeading = None
        
        self.size = realSize
        self._heatRectData = (5, 5, 10, 15)

        self.hitPoints = self.hitPointsLeft = 20
        
        self._baseAC = 1
        self._th0 = None

        self._speech = SpeechCloud(self)
        
        # *** Pathfinding ***
        self.pathfinder = None        # This is inited only after the call onto addToGameLevel

        self.afterInit()
Example #2
0
class Character(GameSprite, Stealth, Warrior):
    """Base character class.
    A GameSprite extension with hit points and other properties for combat.
    """

    _imageDirectory = "charas"
    
    def __init__(self, name, img, containers,
                 realSize=cblocals.TILE_IMAGE_SIZE, speed=150.,
                 attackTime=0.5, afterAttackRestTime=0.2, weaponInAndOut=False, sightRange=200,):
        
        GameSprite.__init__(self, *containers)
        Stealth.__init__(self)
        Warrior.__init__(self, attackTime, afterAttackRestTime)

        self._x = self._y = 0
        self.rect = pygame.Rect( (self.x, self.y), (cblocals.TILE_IMAGE_SIZE) )

        self.name = name
        self.characterType = "Guy"
        self.experienceLevel = 1
        
        self._brain = None
        self._presentationBrain = PresentationStateMachine(self)
        
        self._load_images(img, weaponInAndOut)
        self.lastUsedImage = 'head_south_1'

        self._stepTime = 0
        self._mustChangeImage = False
        self.direction = self._lastUsedDirection = cblocals.DIRECTION_S
        self._isMoving = False
        self.maxSpeed = self._speed = speed
        self.sightRange = sightRange
        self.rest_time_needed = .3
        
        self.side = 'Cheese Boys'
        self._enemyTarget = None
        
        self.navPoint = NavPoint(self)
        self.heading =  None
        
        # From where a succesfull attack is coming
        self.damageHeading = None
        
        self.size = realSize
        self._heatRectData = (5, 5, 10, 15)

        self.hitPoints = self.hitPointsLeft = 20
        
        self._baseAC = 1
        self._th0 = None

        self._speech = SpeechCloud(self)
        
        # *** Pathfinding ***
        self.pathfinder = None        # This is inited only after the call onto addToGameLevel

        self.afterInit()

    def afterInit(self):
        """Called after object creation to do something more specific for different character
        I don't want to overload the __init__ method.
        Base form of this method do nothing at all!
        """
        pass

    def _setSpeed(self, speed):
        self._speed = speed
    def _getSpeed(self):
        speed = self._speed
        healtFactor = self.healtFactor
        if self.stealth:
            speed*=.5
        if healtFactor<.2:
            speed*=.6
        elif healtFactor<.5:
            speed*=.8
        return int(speed)
    speed = property(_getSpeed, _setSpeed, doc="""The character current movement speed""")

    @property
    def isMoving(self):
        return self._isMoving

    def moving(self, new_move_status):
        """Change character movement status"""
        if new_move_status!=self._isMoving:
            self._mustChangeImage = True
        if new_move_status==False:
            # When the character stops from moving, the character heading is changed by the direction faced
            direction = self._getFacedDirection()
            self.heading = self._generateHeadingFromFacindDirection(direction)
        self._isMoving = new_move_status

    def setCombatValues(self, level_bonus, AC):
        """Common method for set all combat values of the character, as far as base AC and TH0 infos are readonly"""
        self._th0 = module_th0.TH0(self, level_bonus)
        self._baseAC = AC

    @property
    def th0(self):
        return self._th0

    @property
    def AC(self):
        """The character current Armour Class value, based on a base value and some other status"""
        base_ac = self._baseAC
        bonus = 0
        active_state = self.active_state
        # BBB: may be very better query the state directly (with a getAcModifier method?)
        if active_state=="retreat":
            bonus = 4
        elif active_state=="attacking":
            bonus = -2
        return base_ac + bonus

    @property
    def healtFactor(self):
        """Service value to get a general healt value for the character.
        @return: a real value between 0 (dead) and 1 (100% healed)
        """
        return float(self.hitPointsLeft)/float(self.hitPoints)
    
    def roll_for_hit(self, target):
        """Common method called to rool a dice and see if a target is hit by the blow"""
        th0 = self._th0
        result = th0.attack(target)
        return result

    def getTip(self):
        """Return tip text, for print it near the character"""
        tip = self._emptyTipStructure.copy()
        tip['text']= self.name or self.characterType
        tip['color']=(255,255,255)
        return tip

    def _load_images(self, img, weaponInAndOut):
        """Load images for this charas: 12 or 24 if used weaponInAndOut (so we need extra images without weapon)"""
        self.images = {}
        directory = self._imageDirectory
        if not weaponInAndOut:
            # 12 images
            self.images['walk_north_1'], self.images['head_north'], self.images['walk_north_2'], self.images['walk_east_1'], \
                self.images['head_east'], self.images['walk_east_2'], self.images['walk_south_1'], self.images['head_south'], \
                self.images['walk_south_2'], self.images['walk_west_1'], self.images['head_west'], \
                self.images['walk_west_2'] = utils.load_image(img, directory, charasFormatImage=True, weaponInAndOut=weaponInAndOut)
        else:
            # 24 images
            self.images['walk_north_1'], self.images['head_north'], self.images['walk_north_2'], self.images['walk_east_1'], \
                self.images['head_east'], self.images['walk_east_2'], self.images['walk_south_1'], self.images['head_south'], \
                self.images['walk_south_2'], self.images['walk_west_1'], self.images['head_west'], \
                self.images['walk_west_2'], \
                self.images['attack_north_1'], self.images['head_attack_north'], self.images['attack_north_2'], self.images['attack_east_1'], \
                self.images['head_attack_east'], self.images['attack_east_2'], self.images['attack_south_1'], self.images['head_attack_south'], \
                self.images['attack_south_2'], self.images['attack_west_1'], self.images['head_attack_west'], \
                self.images['attack_west_2'] = utils.load_image(img, directory, charasFormatImage=True, weaponInAndOut=weaponInAndOut)

    @property
    def brain(self):
        """The brain of the character.
        This will be a PresentationStateMachine instance if a presentation is running
        """
        if self.currentLevel.presentation is not None:
            return self._presentationBrain
        return self._brain

    @property    
    def real_brain(self):
        """The brain of the character. Do not use this property but always the Character.brain.
        Use this only if you need to refer to the character's real brain when a presentation is running
        """
        return self._brain

    def update(self, time_passed):
        """Update method of pygame Sprite class.
        A non playing character check his own AI here.
        """
        GameSprite.update(self, time_passed)
        if self.brain:
            self.brain.think(time_passed)

    def moveBasedOnNavPoint(self, time_passed, destination=None, relative=False):
        """Main method for move character using navPoint infos.
        If a destination is not specified, then the current character navPoint is used.
        @destination: can be None (navPoint will be taken), a point or a Vector2 instance
        @relative: destination is a relative offset from the character position
        """
        # TODO: the current collision checking is broken: a too fast character can pass over an obstacle
        if not destination:
            destination = self.navPoint.get()
            if not destination:
                return
        else:
            if type(destination)==tuple and relative:
                ox, oy = destination
                cx, cy = self.position
                destination = (cx+ox, cy+oy)
            self.navPoint.set(destination)
            destination = self.navPoint.get()
        self.heading = Vector2.from_points(self.position, destination)
        magnitude = self.heading.get_magnitude()
        self.heading.normalize()

        distance = time_passed * self.speed
        # I don't wanna move over the destination!
        if distance>magnitude:
            distance = magnitude
        else:
            # I don't check for a new direction if I'm only fixing the last step distance
            direction = self._generateDirectionFromHeading(self.heading)
            self._checkDirectionChange(direction)

        movement = self.heading * distance
        self._updateStepTime(time_passed)
        x = movement.get_x()
        y = movement.get_y()
        collision = self.checkCollision(x, y)
        if not collision:
            self.move(x, y)
            if self.isNearTo(self.navPoint.as_tuple()):
                self.navPoint.next()
        else:
            #self.navPoint.reroute()
            self.navPoint.reset()

    def hasNoFreeMovementTo(self, target, source=None):
        """Check if the character has NOT free straight movement to a destination point.
        If this is True, the character can't move directly on the vector that link his current
        position to the target due to a collision that will be raised.
        @target: a point coordinate, goal of the movement
        @source: optional parameter to test the free movement from a coordinate different fron the current one.
        @return False if no collision is detected, or the collisions points tuples
        """
        position = self.position
        collide_rect = self.collide_rect
        if not source:
            source = position
        else:
            # I need a collide_rect traslated to the new source
            rx, ry = position[0]-int(source[0]), position[1]-int(source[1])
            collide_rect.move_ip(rx, ry)
        v = Vector2.from_points(source, target)
        magnitude = v.get_magnitude()
        heading = v.normalize()
        distance = min(collide_rect.w, collide_rect.h)
        total_distance = 0
        while True:
            total_distance+=distance
            movement = heading * total_distance
            x = movement.get_x()
            y = movement.get_y()
            collision = self.checkCollision(x, y, silent=True)
            if collision:
                return (x,y)
            if total_distance>=magnitude:
                return False

    def hasFreeSightOn(self, sprite):
        """Check if the target sprite is in sight of the current character
        @sprite: the sprite to be tested.
        @return: True if the target sprite is in sight; False otherwise.
        """
        # BBB: check if this can be enanched someway; the use of 5 pixel is ugly and imperfect
        to_target = Vector2.from_points(self.position, sprite.position)
        magnitude = to_target.get_magnitude()
        # 1 - False if sprite position is outside the character sight
        if self.sightRange<magnitude:
            return False
        # 2 - Now I need to get the line sight on the target
        to_target.normalize()
        magnitude_portion = 5
        visual_obstacles = self.currentLevel['visual_obstacles']
        screen_position = self.toScreenCoordinate()
        while magnitude>0:
            for obstacle in visual_obstacles:
                temp_v = (to_target*magnitude).as_tuple()
                temp_pos = screen_position[0]+temp_v[0], screen_position[1]+temp_v[1]
                if obstacle.collide_rect.collidepoint(temp_pos):
                    logging.debug("%s can't see %s due to the presence of %s" % (self, sprite, obstacle))
                    return False
            magnitude-=magnitude_portion
        return True

    def moveBasedOnHitTaken(self, time_passed):
        """This is similar to moveBasedOnNavPoint, but is called to animate a character hit by a blow"""
        distance = time_passed * self.speed
        movement = self.damageHeading * distance
        x = movement.get_x()
        y = movement.get_y()
        if not self.checkCollision(x, y) and self.checkValidCoord(x, y):
            self.move(x, y)

    def moveBasedOnRetreatAction(self, time_passed):
        """This is similar to moveBasedOnNavPoint, but is called to animate a character that wanna retreat.
        The movement is done in the direction opposite to the heading, but with an offeset of +/- 50° degree.
        """
        heading = -self.heading
        heading.rotate(cbrandom.randint(-50,50))
        distance = time_passed * self.speed
        movement = heading * distance
        x = movement.get_x()
        y = movement.get_y()
        if not self.checkCollision(x, y) and self.checkValidCoord(x, y):
            self.move(x, y)

    @property
    def collide_rect(self):
        """See GameSprite.collide_rect.
        for characters, the foot area is the 25% of the height and 60% in width of the charas, centered on the bottom.
        """
        # BBB: some bigger or different images can behave multiple rects as "foot"?
        rect = self.rect
        ly = rect.bottom
        h = rect.h*0.25
        hy = ly-h
        lx = rect.left + rect.w*0.2 # left + 20-left% of the width
        w = rect.w*0.6
        return pygame.Rect( (lx, hy), (w, h) )

    @property
    def physical_rect(self):
        """See GameSprite.physical_rect.
        Return a rect used for collision in combat (not movement).
        This must be equals to image's character total area.
        """
        rect = self.rect
        diffW = rect.w-self.size[0]
        diffH = rect.h-self.size[1]
        return pygame.Rect( (rect.left+diffW/2, rect.top+diffH), self.size )

    @property
    def heat_rect(self):
        """Return a rect used for collision as heat rect, sensible to attack and other evil effects.
        """
        physical_rect = self.physical_rect
        offsetX, offsetY, w, h = self._heatRectData
        return pygame.Rect( (physical_rect.x+offsetX, physical_rect.y+offsetY), (w, h) )

    def checkValidCoord(self, x=0, y=0):
        """Check if the character coords are valid for current Level
        You can also use x,y adjustement to check a different position of the character, relative
        to the current one.
        """
        r = self.physical_rect.move(x,y)
        r.center = self.currentLevel.transformToLevelCoordinate(r.center)
        return self.currentLevel.checkRectIsInLevel(r)
    
    @property
    @utils.memoize_charasimage
    def image(self):
        """Sprite must have an image property.
        In this way I can control what image return.
        """
        # BBB: it's better to memoize this someway
        if self._attackDirection:
            weaponOut = True
        else:
            weaponOut = False

        if self._isMoving:
            # I'm on move
            if self._mustChangeImage:
                direction = self._getFacedDirection()
                self._mustChangeImage = False
                image = self._getImageFromDirectionWalked(direction, weaponOut)
                self.lastUsedImage = image
            return self._manageImageWithStealth(self.images[self.lastUsedImage])
        else:
            # Stand and wait
            if self._attackDirection:
                # I change the last faced direction because when I right click on a direction when the character isn't moving
                # I wanna face this direction.
                direction = self._lastUsedDirection = self._attackDirection
            else:
                direction = self._lastUsedDirection
            image = self._getImageFromDirectionFaced(direction, weaponOut)
            self.lastUsedImage = image
            return self._manageImageWithStealth(self.images[image])

    def _getFacedDirection(self):
        """If the character was attacking, the direction is the attack direction (_attackDirection).
        Otherwise use the common last used direction _lastUsedDirection.
        """
        if self._attackDirection:
            return self._attackDirection
        return self._lastUsedDirection

    def faceTo(self, direction):
        """Change the character direction faced"""
        self._lastUsedDirection = direction

    def _generateDirectionFromHeading(self, new_heading):
        """Looking at heading, generate a valid direction string"""
        x, y = new_heading.as_tuple()
        if abs(x)<.30 and y<0:
            return cblocals.DIRECTION_N
        if abs(x)<.30 and y>0:
            return cblocals.DIRECTION_S
        if x<0:
            return cblocals.DIRECTION_W
        return cblocals.DIRECTION_E

    def _getWalkImagePrefix(self, direction, weaponOut):
        """Simply return a prefix using to generate the key to retrieve the charas image.
        This prefix is based on the direction of the character but also on the attack state.
        """
        if weaponOut:
            prefix = "attack"
        else:
            prefix = "walk"
        if direction==cblocals.DIRECTION_E or direction==cblocals.DIRECTION_NE or direction==cblocals.DIRECTION_SE:
            return "%s_east_" % prefix
        if direction==cblocals.DIRECTION_W or direction==cblocals.DIRECTION_NW or direction==cblocals.DIRECTION_SW:
            return "%s_west_" % prefix
        if direction==cblocals.DIRECTION_N:
            return "%s_north_" % prefix
        if direction==cblocals.DIRECTION_S:
            return "%s_south_" % prefix
        raise ValueError("Invalid direction %s" % direction)   
    
    def _getImageFromDirectionWalked(self, direction, weaponOut):
        """Using a direction taken get the right image name to display.
        This method check if an attack is currently executed by this character (checking weaponOut). If this is True
        we must return the image facing direction attacked.
        """
        imagePrefix = self._getWalkImagePrefix(direction, weaponOut)
        if self.lastUsedImage.startswith(imagePrefix):
            if self.lastUsedImage.endswith("1"):
                image = self.lastUsedImage[:-1]+"2"
            else:
                image = self.lastUsedImage[:-1]+"1"
        else:
            image = imagePrefix+"1"
        return image

    def _getImageFromDirectionFaced(self, direction, weaponOut):
        """Using a direction, chose the right character non-moving image.
        Use weaponOut to know if an image without weapong carried must be used.
        """
        if weaponOut:
            wstr = "attack_"
        else:
            wstr = ""
        if direction==cblocals.DIRECTION_E or direction==cblocals.DIRECTION_NE or direction==cblocals.DIRECTION_SE:
            image = "head_%seast" % wstr
        elif direction==cblocals.DIRECTION_W or direction==cblocals.DIRECTION_NW or direction==cblocals.DIRECTION_SW:
            image = "head_%swest" % wstr
        elif direction==cblocals.DIRECTION_N:
            image = "head_%snorth" % wstr
        elif direction==cblocals.DIRECTION_S:
            image = "head_%ssouth" % wstr
        else:
            raise ValueError("Invalid direction %s" % direction) 
        return image

    def _generateHeadingFromFacindDirection(self, direction):
        """Passed a direction, return a normalized Vector2 based on the faced direction"""
        if direction==cblocals.DIRECTION_N:
            return Vector2(0.,-1.)
        if direction==cblocals.DIRECTION_E:
            return Vector2(1.,0.)
        if direction==cblocals.DIRECTION_S:
            return Vector2(0.,1.)
        if direction==cblocals.DIRECTION_W:
            return Vector2(-1.,0.)
        raise TypeError("Invalid direction %s" % direction)

    def _updateStepTime(self, time_passed):
        """Update the time passed from the last step ot this character"""
        self._stepTime+=time_passed
        if self._stepTime>=cblocals.CHARAS_STEP_TIME:
            self._stepTime=0
            self._mustChangeImage = True

    def _checkDirectionChange(self, direction):
        """Check if the character movement direction is changed"""
        if direction!=self._lastUsedDirection:
            self._lastUsedDirection = direction
            self._mustChangeImage = True

    def setBrain(self, smBrain):
        """Set a AI StateMachine istance"""
        self._brain = smBrain(self)

    def _set_brain_enabled(self, value):
        self._brain.enabled = value
    brain_enabled = property(lambda self: self._brain.enabled, _set_brain_enabled, doc="""The current character's brain status""")
  
    @property
    def active_state(self):
        """Get the current brain active state"""
        if self.brain:
            return self.brain.active_state.name
        return None
    
    def _setEnemyTarget(self, enemy):
        self._enemyTarget = enemy
    enemyTarget = property(lambda self: self._enemyTarget, _setEnemyTarget, doc="""The character current enemy target""")

    @property
    def isAlive(self):
        """True if the character is alive"""
        return self.hitPointsLeft>0

    def checkAliveState(self):
        """Check the character alive state, or kill it!
        In any case return the alive state as a boolean.
        """
        if not self.isAlive:
            self.kill()
            return False
        return True
    
    def kill(self, corpse=True):
        """Kill the character, removing it from all groups and draw a dead corpse.
        As far as the Character objects are also UniqueObject, we need also to
        unregister a killed sprite from the object_registry.
        @corpse: True (default) for generating the dead corpse; with False the Sprite simply disappear
        """
        GameSprite.kill(self)
        cblocals.object_registry.unregister(self.UID())
        if corpse:
            self.currentLevel.generateDeadSprite(self)

    def getHeadingTo(self, target):
        """Return the heading to a given object or position.
        Object must have a "position" attribute or be a position tuple itself.
        """
        if hasattr(target, 'position'):
            position = target.position
        else:
            position = target
        heading = Vector2.from_points(self.position, position)
        return heading.normalize()

    def generatePhysicalAttackEffect(self, attacker, criticity=None):
        """Called for animate a character hit by a physical blow.
        Character will innaturally move away in a direction opposite to blow origin.
        """
        damage = cbrandom.throwDices(attacker.attackDamage)
        critic = ""

        if criticity and criticity==module_th0.TH0_SURPRISE_HIT:
            critic = "BACKSTAB! "
            damage*=cbrandom.randint(3,4)
        elif criticity and criticity==module_th0.TH0_HIT_CRITICAL:
            if cbrandom.randint(1,2)==1:
                self.shout(_("Ouch!"))
            damage = int(damage * 1.5)
            critic = "CRITICAL! "
        elif criticity and criticity==module_th0.TH0_HIT_SURPRISE_CRITICAL:
            damage*=cbrandom.randint(4,6)
            critic = "DEADLY! "

        self.hitPointsLeft-= damage
        print "  %s%s wounded for %s points. %s left" % (critic, self.name, damage, self.hitPointsLeft)
        # Below I use lastAttackHeading because may be that attackHeading is now None (enemy ends the attack)
        self.damageHeading = attacker.lastAttackHeading
        if self.brain:
            self.brain.setState("hitten")
        if not self.checkAliveState():
            print "%s is dead." % self.name
        elif attacker.stealth and not self.canSeeHiddenCharacter(attacker):
            # The attacker was hidden in shadows and unseen, but the character (the target) is not dead! Now the character can see it!
            self.noticeForHiddenCharacter(attacker)
        # however the character has been hit, so I need to reset it's stealth state
        if self.stealth:
            self.stealth = False

    @classmethod
    def getHealtColor(cls, total, left):
        """Given two value return a tuple RBG that repr a color for points left.
        More point left, more the color will be green.
        With point decrease, this will lead to red.
        """
        # 255 : total = x : left
        v1 = 255*left/total
        return (255-v1,v1,0)

    def drawPointsInfos(self, surface):
        """Draw infos about this character point left on the surface"""
        hitPoints = self.hitPoints
        hitPointsLeft = self.hitPointsLeft
        pr = self.physical_rect
        # hitPoints : pr.height = hitPointsLeft : x
        topright = (pr.topright[0], pr.bottomright[1] - (pr.height * hitPointsLeft / hitPoints) )
        pygame.draw.line(surface, self.getHealtColor(hitPoints, hitPointsLeft), pr.bottomright, topright, 3)

    # ******* Talking methods *******
    def say(self, text, additional_time=0, silenceFirst=False):
        """Say something, displaying the speech cloud"""
        if silenceFirst:
            self._speech.endSpeech()
        self._speech.text = text
        if additional_time:
            self._speech.additionalTime(additional_time)

    def shout(self, text, additional_time=0, silenceFirst=False):
        """As say() but with uppercase text"""
        if silenceFirst:
            self._speech.endSpeech()
        self._speech.text = text.upper()
        if additional_time:
            self._speech.additionalTime(additional_time)
        event = pygame.event.Event(cblocals.SHOUT_EVENT, {'character':self, 'position':self.position, 'text': text})
        pygame.event.post(event)

    def shutup(self):
        """Immediatly shut up the character"""
        self._speech.endSpeech()
     # *******

    def addToGameLevel(self, level, firstPosition):
        """Call the GameSprite.addToGameLevel but also init the pathfinder object"""
        GameSprite.addToGameLevel(self, level, firstPosition)
        self.pathfinder = PathFinder(self.currentLevel.grid_map_successors,
                                     self.currentLevel.grid_map_move_cost,
                                     self.currentLevel.grid_map_heuristic_to_goal)

    def __str__(self):
        return "%s <%s>" % (self.name, self.UID())

    def __repr__(self):
        return str(self)