all repos — Legends-RPG @ 05d9e3d3d1f5fd253dc04a612313eac3b634def0

A fantasy mini-RPG built with Python and Pygame.

data/components/person.py (view raw)

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