all repos — Legends-RPG @ ab397e926c7e59f522b1eada41f84bbf27bf1bdd

A fantasy mini-RPG built with Python and Pygame.

data/components/person.py (view raw)

  1from __future__ import division
  2from itertools import izip
  3import math, random, copy, sys
  4import pygame as pg
  5from .. import setup, observer
  6from .. import constants as c
  7
  8#Python 2/3 compatibility.
  9if sys.version_info[0] == 2:
 10    range = xrange
 11
 12
 13class Person(pg.sprite.Sprite):
 14    """Base class for all world characters
 15    controlled by the computer"""
 16
 17    def __init__(self, sheet_key, x, y, direction='down', state='resting', index=0):
 18        super(Person, self).__init__()
 19        self.alpha = 255
 20        self.name = sheet_key
 21        self.get_image = setup.tools.get_image
 22        self.spritesheet_dict = self.create_spritesheet_dict(sheet_key)
 23        self.animation_dict = self.create_animation_dict()
 24        self.index = index
 25        self.direction = direction
 26        self.image_list = self.animation_dict[self.direction]
 27        self.image = self.image_list[self.index]
 28        self.rect = self.image.get_rect(left=x, top=y)
 29        self.origin_pos = self.rect.topleft
 30        self.state_dict = self.create_state_dict()
 31        self.vector_dict = self.create_vector_dict()
 32        self.x_vel = 0
 33        self.y_vel = 0
 34        self.timer = 0.0
 35        self.move_timer = 0.0
 36        self.current_time = 0.0
 37        self.state = state
 38        self.blockers = self.set_blockers()
 39        self.location = self.get_tile_location()
 40        self.dialogue = ['Location: ' + str(self.location)]
 41        self.default_direction = direction
 42        self.item = None
 43        self.wander_box = self.make_wander_box()
 44        self.observers = [observer.SoundEffects()]
 45        self.health = 0
 46        self.death_image = pg.transform.scale2x(self.image)
 47        self.battle = None
 48
 49    def create_spritesheet_dict(self, sheet_key):
 50        """
 51        Make a dictionary of images from sprite sheet.
 52        """
 53        image_list = []
 54        image_dict = {}
 55        sheet = setup.GFX[sheet_key]
 56
 57        image_keys = ['facing up 1', 'facing up 2',
 58                      'facing down 1', 'facing down 2',
 59                      'facing left 1', 'facing left 2',
 60                      'facing right 1', 'facing right 2']
 61
 62        for row in range(2):
 63            for column in range(4):
 64                image_list.append(
 65                    self.get_image(column*32, row*32, 32, 32, sheet))
 66
 67        for key, image in izip(image_keys, image_list):
 68            image_dict[key] = image
 69
 70        return image_dict
 71
 72    def create_animation_dict(self):
 73        """
 74        Return a dictionary of image lists for animation.
 75        """
 76        image_dict = self.spritesheet_dict
 77
 78        left_list = [image_dict['facing left 1'], image_dict['facing left 2']]
 79        right_list = [image_dict['facing right 1'], image_dict['facing right 2']]
 80        up_list = [image_dict['facing up 1'], image_dict['facing up 2']]
 81        down_list = [image_dict['facing down 1'], image_dict['facing down 2']]
 82
 83        direction_dict = {'left': left_list,
 84                          'right': right_list,
 85                          'up': up_list,
 86                          'down': down_list}
 87
 88        return direction_dict
 89
 90    def create_state_dict(self):
 91        """
 92        Return a dictionary of all state methods.
 93        """
 94        state_dict = {'resting': self.resting,
 95                      'moving': self.moving,
 96                      'animated resting': self.animated_resting,
 97                      'autoresting': self.auto_resting,
 98                      'automoving': self.auto_moving,
 99                      'battle resting': self.battle_resting,
100                      'attack': self.attack,
101                      'enemy attack': self.enemy_attack,
102                      c.RUN_AWAY: self.run_away,
103                      c.VICTORY_DANCE: self.victory_dance,
104                      c.KNOCK_BACK: self.knock_back,
105                      c.FADE_DEATH: self.fade_death}
106
107        return state_dict
108
109    def create_vector_dict(self):
110        """
111        Return a dictionary of x and y velocities set to
112        direction keys.
113        """
114        vector_dict = {'up': (0, -1),
115                       'down': (0, 1),
116                       'left': (-1, 0),
117                       'right': (1, 0)}
118
119        return vector_dict
120
121    def update(self, current_time, *args):
122        """
123        Update sprite.
124        """
125        self.blockers = self.set_blockers()
126        self.current_time = current_time
127        self.image_list = self.animation_dict[self.direction]
128        state_function = self.state_dict[self.state]
129        state_function()
130        self.location = self.get_tile_location()
131
132    def set_blockers(self):
133        """
134        Sets blockers to prevent collision with other sprites.
135        """
136        blockers = []
137
138        if self.state == 'resting' or self.state == 'autoresting':
139            blockers.append(pg.Rect(self.rect.x, self.rect.y, 32, 32))
140
141        elif self.state == 'moving' or self.state == 'automoving':
142            if self.rect.x % 32 == 0:
143                tile_float = self.rect.y / float(32)
144                tile1 = (self.rect.x, math.ceil(tile_float)*32)
145                tile2 = (self.rect.x, math.floor(tile_float)*32)
146                tile_rect1 = pg.Rect(tile1[0], tile1[1], 32, 32)
147                tile_rect2 = pg.Rect(tile2[0], tile2[1], 32, 32)
148                blockers.extend([tile_rect1, tile_rect2])
149
150            elif self.rect.y % 32 == 0:
151                tile_float = self.rect.x / float(32)
152                tile1 = (math.ceil(tile_float)*32, self.rect.y)
153                tile2 = (math.floor(tile_float)*32, self.rect.y)
154                tile_rect1 = pg.Rect(tile1[0], tile1[1], 32, 32)
155                tile_rect2 = pg.Rect(tile2[0], tile2[1], 32, 32)
156                blockers.extend([tile_rect1, tile_rect2])
157
158        return blockers
159
160    def get_tile_location(self):
161        """
162        Convert pygame coordinates into tile coordinates.
163        """
164        if self.rect.x == 0:
165            tile_x = 0
166        elif self.rect.x % 32 == 0:
167            tile_x = (self.rect.x / 32)
168        else:
169            tile_x = 0
170
171        if self.rect.y == 0:
172            tile_y = 0
173        elif self.rect.y % 32 == 0:
174            tile_y = (self.rect.y / 32)
175        else:
176            tile_y = 0
177
178        return [tile_x, tile_y]
179
180
181    def make_wander_box(self):
182        """
183        Make a list of rects that surround the initial location
184        of a sprite to limit his/her wandering.
185        """
186        x = int(self.location[0])
187        y = int(self.location[1])
188        box_list = []
189        box_rects = []
190
191        for i in range(x-3, x+4):
192            box_list.append([i, y-3])
193            box_list.append([i, y+3])
194
195        for i in range(y-2, y+3):
196            box_list.append([x-3, i])
197            box_list.append([x+3, i])
198
199        for box in box_list:
200            left = box[0]*32
201            top = box[1]*32
202            box_rects.append(pg.Rect(left, top, 32, 32))
203
204        return box_rects
205
206    def resting(self):
207        """
208        When the Person is not moving between tiles.
209        Checks if the player is centered on a tile.
210        """
211        self.image = self.image_list[self.index]
212
213        if self.rect.y % 32 != 0:
214            self.correct_position(self.rect.y)
215        if self.rect.x % 32 != 0:
216            self.correct_position(self.rect.x)
217
218    def moving(self):
219        """
220        Increment index and set self.image for animation.
221        """
222        self.animation()
223        assert(self.rect.x % 32 == 0 or self.rect.y % 32 == 0), \
224            'Not centered on tile'
225
226    def animated_resting(self):
227        self.animation(500)
228
229    def animation(self, freq=100):
230        """
231        Adjust sprite image frame based on timer.
232        """
233        if (self.current_time - self.timer) > freq:
234            if self.index < (len(self.image_list) - 1):
235                self.index += 1
236            else:
237                self.index = 0
238            self.timer = self.current_time
239
240        self.image = self.image_list[self.index]
241
242    def begin_moving(self, direction):
243        """
244        Transition the player into the 'moving' state.
245        """
246        self.direction = direction
247        self.image_list = self.animation_dict[direction]
248        self.timer = self.current_time
249        self.move_timer = self.current_time
250        self.state = 'moving'
251
252        if self.rect.x % 32 == 0:
253            self.y_vel = self.vector_dict[self.direction][1]
254        if self.rect.y % 32 == 0:
255            self.x_vel = self.vector_dict[self.direction][0]
256
257
258    def begin_resting(self):
259        """
260        Transition the player into the 'resting' state.
261        """
262        self.state = 'resting'
263        self.index = 1
264        self.x_vel = self.y_vel = 0
265
266    def begin_auto_moving(self, direction):
267        """
268        Transition sprite to a automatic moving state.
269        """
270        self.direction = direction
271        self.image_list = self.animation_dict[direction]
272        self.state = 'automoving'
273        self.x_vel = self.vector_dict[direction][0]
274        self.y_vel = self.vector_dict[direction][1]
275        self.move_timer = self.current_time
276
277    def begin_auto_resting(self):
278        """
279        Transition sprite to an automatic resting state.
280        """
281        self.state = 'autoresting'
282        self.index = 1
283        self.x_vel = self.y_vel = 0
284        self.move_timer = self.current_time
285
286
287    def auto_resting(self):
288        """
289        Determine when to move a sprite from resting to moving in a random
290        direction.
291        """
292        self.image_list = self.animation_dict[self.direction]
293        self.image = self.image_list[self.index]
294
295        if self.rect.y % 32 != 0:
296            self.correct_position(self.rect.y)
297        if self.rect.x % 32 != 0:
298            self.correct_position(self.rect.x)
299
300        if (self.current_time - self.move_timer) > 2000:
301            direction_list = ['up', 'down', 'left', 'right']
302            random.shuffle(direction_list)
303            direction = direction_list[0]
304            self.begin_auto_moving(direction)
305            self.move_timer = self.current_time
306
307    def correct_position(self, rect_pos):
308        """
309        Adjust sprite position to be centered on tile.
310        """
311        diff = rect_pos % 32
312        if diff <= 16:
313            rect_pos - diff
314        else:
315            rect_pos + diff
316 
317
318    def battle_resting(self):
319        """
320        Player stays still during battle state unless he attacks.
321        """
322        pass
323
324    def enter_attack_state(self, enemy):
325        """
326        Set values for attack state.
327        """
328        self.notify(c.SWORD)
329        self.attacked_enemy = enemy
330        self.x_vel = -5
331        self.state = 'attack'
332
333
334    def attack(self):
335        """
336        Player does an attack animation.
337        """
338        FAST_FORWARD = -5
339        FAST_BACK = 5
340
341        self.rect.x += self.x_vel
342
343        if self.x_vel == FAST_FORWARD:
344            self.image = self.spritesheet_dict['facing left 1']
345            self.image = pg.transform.scale2x(self.image)
346            if self.rect.x <= self.origin_pos[0] - 110:
347                self.x_vel = FAST_BACK
348                self.notify(c.ENEMY_DAMAGED)
349        else:
350            if self.rect.x >= self.origin_pos[0]:
351                self.rect.x = self.origin_pos[0]
352                self.x_vel = 0
353                self.state = 'battle resting'
354                self.image = self.spritesheet_dict['facing left 2']
355                self.image = pg.transform.scale2x(self.image)
356                self.notify(c.PLAYER_FINISHED_ATTACK)
357
358    def enter_enemy_attack_state(self):
359        """
360        Set values for enemy attack state.
361        """
362        self.x_vel = -5
363        self.state = 'enemy attack'
364        self.origin_pos = self.rect.topleft
365        self.move_counter = 0
366
367    def enemy_attack(self):
368        """
369        Enemy does an attack animation.
370        """
371        FAST_LEFT = -5
372        FAST_RIGHT = 5
373        STARTX = self.origin_pos[0]
374
375        self.rect.x += self.x_vel
376
377        if self.move_counter == 3:
378            self.x_vel = 0
379            self.state = 'battle resting'
380            self.rect.x = STARTX
381            self.notify(c.PLAYER_DAMAGED)
382
383        elif self.x_vel == FAST_LEFT:
384            if self.rect.x <= (STARTX - 15):
385                self.x_vel = FAST_RIGHT
386        elif self.x_vel == FAST_RIGHT:
387            if self.rect.x >= (STARTX + 15):
388                self.move_counter += 1
389                self.x_vel = FAST_LEFT
390
391    def auto_moving(self):
392        """
393        Animate sprite and check to stop.
394        """
395        self.animation()
396
397        assert(self.rect.x % 32 == 0 or self.rect.y % 32 == 0), \
398            'Not centered on tile'
399
400    def notify(self, event):
401        """
402        Notify all observers of events.
403        """
404        for observer in self.observers:
405            observer.on_notify(event)
406
407    def calculate_hit(self, armor_list, inventory):
408        """
409        Calculate hit strength based on attack stats.
410        """
411        armor_power = 0
412        for armor in armor_list:
413            armor_power += inventory[armor]['power']
414        max_strength = max(1, (self.level * 5) - armor_power)
415        min_strength = 0
416        return random.randint(min_strength, max_strength)
417
418    def run_away(self):
419        """
420        Run away from battle state.
421        """
422        X_VEL = 5
423        self.rect.x += X_VEL
424        self.direction = 'right'
425        self.small_image_list = self.animation_dict[self.direction]
426        self.image_list = []
427        for image in self.small_image_list:
428            self.image_list.append(pg.transform.scale2x(image))
429        self.animation()
430
431    def victory_dance(self):
432        """
433        Post Victory Dance.
434        """
435        self.small_image_list = self.animation_dict[self.direction]
436        self.image_list = []
437        for image in self.small_image_list:
438            self.image_list.append(pg.transform.scale2x(image))
439        self.animation(500)
440
441    def knock_back(self):
442        """
443        Knock back when hit.
444        """
445        FORWARD_VEL = -2
446
447        self.rect.x += self.x_vel
448
449        if self.name == 'player':
450            if self.rect.x >= (self.origin_pos[0] + 10):
451                self.x_vel = FORWARD_VEL
452            elif self.rect.x <= self.origin_pos[0]:
453                self.rect.x = self.origin_pos[0]
454                self.state = 'battle resting'
455                self.x_vel = 0
456        else:
457            if self.rect.x <= (self.origin_pos[0] - 10):
458                self.x_vel = 2
459            elif self.rect.x >= self.origin_pos[0]:
460                self.rect.x = self.origin_pos[0]
461                self.state = 'battle resting'
462                self.x_vel = 0
463
464    def fade_death(self):
465        """
466        Make character become transparent in death.
467        """
468        self.image = pg.Surface((64, 64)).convert()
469        self.image.set_colorkey(c.BLACK)
470        self.image.set_alpha(self.alpha)
471        self.image.blit(self.death_image, (0, 0))
472        self.alpha -= 8
473        if self.alpha <= 0:
474            self.kill()
475            self.notify(c.ENEMY_DEAD)
476
477
478    def enter_knock_back_state(self):
479        """
480        Set values for entry to knock back state.
481        """
482        if self.name == 'player':
483            self.x_vel = 4
484        else:
485            self.x_vel = -4
486
487        self.state = c.KNOCK_BACK
488        self.origin_pos = self.rect.topleft
489
490
491class Player(Person):
492    """
493    User controlled character.
494    """
495
496    def __init__(self, direction, game_data, x=0, y=0, state='resting', index=0):
497        super(Player, self).__init__('player', x, y, direction, state, index)
498        self.damaged = False
499        self.healing = False
500        self.damage_alpha = 0
501        self.healing_alpha = 0
502        self.fade_in = True
503        self.game_data = game_data
504        self.index = 1
505        self.image = self.image_list[self.index]
506
507    @property
508    def level(self):
509        """
510        Make level property equal to player level in game_data.
511        """
512        return self.game_data['player stats']['Level']
513
514
515    def create_vector_dict(self):
516        """Return a dictionary of x and y velocities set to
517        direction keys."""
518        vector_dict = {'up': (0, -2),
519                       'down': (0, 2),
520                       'left': (-2, 0),
521                       'right': (2, 0)}
522
523        return vector_dict
524
525    def update(self, keys, current_time):
526        """Updates player behavior"""
527        self.current_time = current_time
528        self.damage_animation()
529        self.healing_animation()
530        self.blockers = self.set_blockers()
531        self.keys = keys
532        self.check_for_input()
533        state_function = self.state_dict[self.state]
534        state_function()
535        self.location = self.get_tile_location()
536
537    def damage_animation(self):
538        """
539        Put a red overlay over sprite to indicate damage.
540        """
541        if self.damaged:
542            self.image = copy.copy(self.spritesheet_dict['facing left 2'])
543            self.image = pg.transform.scale2x(self.image).convert_alpha()
544            damage_image = copy.copy(self.image).convert_alpha()
545            damage_image.fill((255, 0, 0, self.damage_alpha), special_flags=pg.BLEND_RGBA_MULT)
546            self.image.blit(damage_image, (0, 0))
547            if self.fade_in:
548                self.damage_alpha += 25
549                if self.damage_alpha >= 255:
550                    self.fade_in = False
551                    self.damage_alpha = 255
552            elif not self.fade_in:
553                self.damage_alpha -= 25
554                if self.damage_alpha <= 0:
555                    self.damage_alpha = 0
556                    self.damaged = False
557                    self.fade_in = True
558                    self.image = self.spritesheet_dict['facing left 2']
559                    self.image = pg.transform.scale2x(self.image)
560
561    def healing_animation(self):
562        """
563        Put a green overlay over sprite to indicate healing.
564        """
565        if self.healing:
566            self.image = copy.copy(self.spritesheet_dict['facing left 2'])
567            self.image = pg.transform.scale2x(self.image).convert_alpha()
568            healing_image = copy.copy(self.image).convert_alpha()
569            healing_image.fill((0, 255, 0, self.healing_alpha), special_flags=pg.BLEND_RGBA_MULT)
570            self.image.blit(healing_image, (0, 0))
571            if self.fade_in:
572                self.healing_alpha += 25
573                if self.healing_alpha >= 255:
574                    self.fade_in = False
575                    self.healing_alpha = 255
576            elif not self.fade_in:
577                self.healing_alpha -= 25
578                if self.healing_alpha <= 0:
579                    self.healing_alpha = 0
580                    self.healing = False
581                    self.fade_in = True
582                    self.image = self.spritesheet_dict['facing left 2']
583                    self.image = pg.transform.scale2x(self.image)
584
585    def check_for_input(self):
586        """Checks for player input"""
587        if self.state == 'resting':
588            if self.keys[pg.K_UP]:
589                self.begin_moving('up')
590            elif self.keys[pg.K_DOWN]:
591                self.begin_moving('down')
592            elif self.keys[pg.K_LEFT]:
593                self.begin_moving('left')
594            elif self.keys[pg.K_RIGHT]:
595                self.begin_moving('right')
596
597    def calculate_hit(self):
598        """
599        Calculate hit strength based on attack stats.
600        """
601        weapon = self.game_data['player inventory']['equipped weapon']
602        weapon_power = self.game_data['player inventory'][weapon]['power']
603        max_strength = weapon_power 
604        min_strength = max_strength - 7
605        return random.randint(min_strength, max_strength)
606
607
608class Enemy(Person):
609    """
610    Enemy sprite.
611    """
612    def __init__(self, sheet_key, x, y, direction='down', state='resting', index=0):
613        super(Enemy, self).__init__(sheet_key, x, y, direction, state, index)
614        self.level = 1
615        self.type = 'enemy'
616
617
618class Chest(Person):
619    """
620    Treasure chest that contains items to collect.
621    """
622    def __init__(self, x, y, id):
623        super(Chest, self).__init__('treasurechest', x, y)
624        self.spritesheet_dict = self.make_image_dict()
625        self.image_list = self.make_image_list()
626        self.image = self.image_list[self.index]
627        self.rect = self.image.get_rect(x=x, y=y)
628        self.id = id
629
630    def make_image_dict(self):
631        """
632        Make a dictionary for the sprite's images.
633        """
634        sprite_sheet = setup.GFX['treasurechest']
635        image_dict = {'closed': self.get_image(0, 0, 32, 32, sprite_sheet),
636                      'opened': self.get_image(32, 0, 32, 32, sprite_sheet)}
637
638        return image_dict
639
640    def make_image_list(self):
641        """
642        Make the list of two images for the chest.
643        """
644        image_list = [self.spritesheet_dict['closed'],
645                      self.spritesheet_dict['opened']]
646
647        return image_list
648
649    def update(self, current_time, *args):
650        """Implemented by inheriting classes"""
651        self.blockers = self.set_blockers()
652        self.current_time = current_time
653        state_function = self.state_dict[self.state]
654        state_function()
655        self.location = self.get_tile_location()
656
657
658
659