all repos — Legends-RPG @ b93c81318903506cb3e9e20762b751b003612b8d

A fantasy mini-RPG built with Python and Pygame.

data/components/person.py (view raw)

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