all repos — Legends-RPG @ a452513181f1eef80107430ce479b678dc2fa21c

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