Return to repo list

heart-of-gold

Tactical RPG written in python, using pygame.
Return to HMagellan.com

piece.py (35175B)


      1 import pygame, os, json, random, copy
      2 from . import manager, entity
      3 from .constants import *
      4 
      5 ############
      6 # piece.py #
      7 ############
      8 
      9 # TODO: The ui elements controlled here should definitely be moved to TurnManager
     10 
     11 # This file contains the following:
     12 #   1.  The PieceManager object, which manages pieces on the game board
     13 #   2.  The Piece entity child object, which represents a piece on the game board
     14 #   3.  The TileCursor entity child object, which acts as a cursor on the board and is managed by PieceManager
     15 
     16 ######################################
     17 # Section 1 - The PieceManager class #
     18 ######################################
     19 
     20 class PieceManager(manager.Manager):
     21     """
     22     PieceManager acts as a manager for all Pieces (Entities
     23     representing playable characters on the game board), as
     24     well as for the TileCursor object that is used to interact
     25     with pieces and tiles.
     26     """
     27 
     28     def __init__(self, game, bus, camera, name):
     29 
     30         # Parent initialization
     31         super().__init__(game, bus, camera, name)
     32 
     33         # Entity values
     34         self.pieces = pygame.sprite.LayeredDirty()
     35         self.selected_piece = None
     36 
     37         # Cursor values
     38         self.tile_cursor = None
     39         self.plumb_bob = None
     40 
     41     def add_piece(self, piece):
     42         """
     43         Add an piece to the pieces group.
     44         """
     45         self.pieces.add(piece)
     46 
     47     def reset_state(self):
     48         """
     49         Reset the piece manager to a just-loaded
     50         state.
     51         """
     52         self.pieces = pygame.sprite.LayeredDirty()
     53         self.selected_piece = None
     54         self.load_ui_elements()
     55 
     56     def load_pieces_from_def(self, definition):
     57         """
     58         Loaded one or more pieces from a definition
     59         dictionary. A piece definition is a subcomponent
     60         of a larger definition dict, likely loaded from
     61         JSON.
     62         """
     63         # TODO: There could be some type-checking for a valid definition
     64         #       here. This should probably be so for all methods that load
     65         #       from definition.
     66         for p in definition:
     67             # NOTE: This is one of those screwy things you don't often run into, but the copy module is
     68             #       needed here because of the pointer nature of python vals. Normally a shallow copy (e.g. slices,
     69             #       comprehensions, etc.) is enough, but in this case, with a dict composed of two other dicts,
     70             #       a deep copy must be made because we are trying to operate on mutable compound values once they
     71             #       are passed to the piece objects.
     72             work = copy.deepcopy(definition[p])
     73             n_sheet = self.bus.fetch("sheet_manager", "sheets")[work["sheet"]]
     74             n_anim = n_sheet.animations[work["animation"]]
     75             n_name = work["name"]
     76             n_class = work["class"]
     77             n_pic = work["pictures"]
     78             n_lvl = work["level"]
     79             n_exp = work["exp"]
     80             n_rank = work["rank"]
     81             n_nstats = work["normal_stats"]
     82             n_mod = work["stat_mod"]
     83             n_dist = work["stat_dist"]
     84             n_aff = work["affinity"]
     85             n_tise = work["expertise"]
     86             n_team = work["team"]
     87             n_equip = work["equipment"]
     88             n_growth = work["growth"]
     89             np = Piece(n_sheet, (0, 0), n_anim, True, self, n_name, n_class, n_pic, n_lvl, n_exp, n_rank, n_aff, n_tise, False, n_nstats, n_mod, n_dist, 
     90                        n_team, n_equip, n_growth)
     91             np.assign_tile(tuple(work["tile"]), self.bus.fetch("board_manager", "board_tile_layer")[tuple(work["tile"])])
     92             np.snap_to_tile()
     93             self.add_piece(np)
     94 
     95     def load_pieces_from_file(self, filename):
     96         """
     97         Load pieces from a given JSON file,
     98         cross-referenced against the piece
     99         database.
    100         """
    101         # Setup
    102         foldname = ""
    103         full_def = {}
    104 
    105         # Ensure the dir and file names match our structure
    106         if filename[::-4] != ".json":
    107             foldname = filename
    108             filename = filename + ".json"
    109         else:
    110             foldname = filename[::-4]
    111 
    112         # Next, load up the JSON file
    113         with open(os.path.join(BOARD_PATH, foldname, filename)) as jf: jsondef = json.load(jf)
    114         
    115         # Now, create the full definition given the JSON and the character database
    116         for p in jsondef:
    117             temp = CHARBASE[jsondef[p]["template"]]
    118             full_def[p] = {
    119                 "name" : jsondef[p]["name"],
    120                 "sheet" : jsondef[p]["replace"]["sheet"] if "sheet" in jsondef[p]["replace"].keys() else temp["sheet"],
    121                 "sprite" : (0, 0),
    122                 "animation" : jsondef[p]["replace"]["animation"] if "animation" in jsondef[p]["replace"].keys() else "stand_L",
    123                 "animated" : True,
    124                 "passable" : False,
    125                 "pictures" : jsondef[p]["replace"]["pictures"] if "pictures" in jsondef[p]["replace"].keys() else temp["pictures"],
    126                 "class" : temp["class"],
    127                 "level" : jsondef[p]["level"],
    128                 "exp" : jsondef[p]["exp"],
    129                 "rank" : jsondef[p]["rank"],
    130                 "normal_stats" : temp["stats"],
    131                 "stat_mod" : jsondef[p]["stat_mod"],
    132                 "stat_dist" : jsondef[p]["stat_dist"],
    133                 "affinity" : jsondef[p]["replace"]["affinity"] if "affinity" in jsondef[p]["replace"] else temp["affinity"],
    134                 "expertise" : temp["expertise"],
    135                 "team" : jsondef[p]["team"],
    136                 "tile" : jsondef[p]["tile"],
    137                 "equipment" : jsondef[p]["equipment"],
    138                 "growth" : temp["growth"]
    139             }
    140 
    141         # Lastly, load the def as real pieces
    142         self.load_pieces_from_def(full_def)
    143 
    144     def load_ui_elements(self):
    145         """
    146         Load a TileCursor object to highlight selected tiles,
    147         and a plumb bob to hover over the acting piece.
    148         """
    149         try:
    150             sh = self.bus.fetch("sheet_manager", "sheets")
    151         except ManagerBusError:
    152             return
    153 
    154         self.tile_cursor = TileCursor(sh["cursor1"], (0, 0), sh["cursor1"].animations["pulse"], True)
    155         self.plumb_bob = entity.Entity(sh["plumb_bob_1"], (0, 0), sh["plumb_bob_1"].animations["spin"], True)
    156 
    157     def get_piece_by_name(self, name):
    158         """
    159         Returns a piece matching name, or None if no such piece
    160         is found.
    161         """
    162         piece = None
    163         for p in self.pieces:
    164             if p.name == name:
    165                 piece = p
    166                 break
    167         return piece
    168 
    169     def get_piece_by_tile(self, tile_pos):
    170         """
    171         Return the first piece found that is located at tile_pos,
    172         or None if no piece is found.
    173         """
    174         found_piece = None
    175         for p in self.pieces:
    176             if p.tile_pos == (tile_pos[0], tile_pos[1]):
    177                 found_piece = p
    178         return found_piece
    179 
    180     def position_tile_cursor(self, tile_pos):
    181         """
    182         Snap the TileCursor object's position to 'tile_pos' and set 
    183         the tile GID there as the current tile_cursor's selected tile.
    184         """
    185         self.tile_cursor.assign_tile(tile_pos, self.bus.fetch("board_manager", "board_tile_layer")[tile_pos])
    186         self.tile_cursor.snap_to_tile()
    187 
    188     def select_piece_with_tile_cursor(self):
    189         """
    190         Select the piece under the tile cursor right now. If
    191         no piece is under the tile cursor, the selected piece
    192         should become None.
    193         """
    194         self.selected_piece = self.get_piece_by_tile(self.tile_cursor.tile_pos)
    195         self.expose()
    196 
    197     def set_piece_move_to_tile_path(self, piece, path):
    198         """
    199         Assigns a path to the given piece.
    200         """
    201         if piece != None and path != None:
    202             piece.set_move_along_tile_path(path)
    203 
    204     def get_piece_max_legal_move(self, piece):
    205         """
    206         Calculate the absolute maximum legal move of a piece.
    207         Returns a list of maximum legal moved.
    208         """
    209         m = piece.active_stats["MOVE"]
    210         legal_moves = []
    211         dims = self.bus.fetch("board_manager", "board_dimensions")
    212         for x in range(0, dims[0]):
    213             for y in range(0, dims[1]):
    214                 mx = piece.tile_pos[0] - x
    215                 my = piece.tile_pos[1] - y
    216                 if (abs(mx) + abs(my)) <= m:
    217                     legal_moves.append((x, y))
    218         return legal_moves
    219 
    220     def execute_attack(self, attacker, target):
    221         """
    222         Execute an attack between two pieces.
    223         """
    224         if attacker in self.pieces and target in self.pieces:
    225             attacker.set_attack_action(target)
    226 
    227     def execute_guard(self, piece):
    228         """
    229         Execute a guard action by piece.
    230         """
    231         if piece in self.pieces and not piece.guarding:
    232             piece.set_guard_action()
    233 
    234     def kill_piece(self, piece):
    235         """
    236         Remove a piece from everything.
    237         """
    238         if piece in self.pieces:
    239             self.pieces.remove(piece)
    240 
    241     def center_plumb_bob(self):
    242         """
    243         Center the plumb bob to the current
    244         active piece.
    245         """
    246         if self.plumb_bob != None:
    247             try:
    248                 ap = self.bus.fetch("turn_manager", "active_piece")
    249             except ManagerBusError:
    250                 ap = None
    251             if ap != None:
    252                 self.plumb_bob.set_center_position((ap.rect.center[0], ap.rect.center[1] - (3 * (TILE_HEIGHT // 4))))
    253 
    254     def expose(self):
    255         """
    256         Expose info about pieces to the ManagerBus.
    257         """
    258         data = {
    259             "selected_piece" : self.selected_piece,
    260             "tile_cursor" : self.tile_cursor,
    261             "pieces" : self.pieces,
    262             "pieces_by_name" : { p.name : p for p in self.pieces },
    263             "pieces_by_tile_pos" : { p.tile_pos : p for p in self.pieces }
    264         }
    265         self.bus.record(self.name, data)
    266 
    267     def update_managed(self, surface = None):
    268         """
    269         Update the pieces and tile cursor.
    270         """
    271         if surface != None:
    272             self.pieces.update(surface, self.game.control_mode not in PAUSE_MODES)
    273             if self.game.control_mode not in PAUSE_MODES:
    274                 self.tile_cursor.update(surface)
    275             self.center_plumb_bob()
    276             if self.game.control_mode in PLUMB_BOB_DRAW_TURN_MODES:
    277                 self.plumb_bob.update(surface)
    278 
    279 ###############################
    280 # Section 2 - The Piece class #
    281 ###############################
    282 
    283 class Piece(entity.Entity):
    284     """
    285     Object that represents a playable piece on the board. Mostly
    286     only differs from entity in that it expects the standard
    287     animation format to allow for facing, moving, etc. Has some
    288     sligthly modified move_motion and set_motion methods. Also
    289     has much more support for existing on a Board and amongst
    290     tiles.
    291     """
    292     
    293     def __init__(self, sheet, sprite = (0, 0), animation = None, animated = False, 
    294                  manager = None, name = None, classname = None, pictures = None, level = None, exp = None, rank = None, affinity = None, 
    295                  expertise = None, passable = False, normal_stats = None, stat_mod = None, stat_dist = None, team = None, equipment = None, 
    296                  growth = None):
    297 
    298         # Parent initialization
    299         super().__init__(sheet, sprite, animation, animated)
    300 
    301         # Face settings
    302         self.facing = FACE_DIR.L
    303 
    304         # Others
    305         self.manager = manager
    306         self.name = name
    307         self.classname = classname
    308         self.level = 1
    309         self.rank = rank
    310         self.exp = exp
    311         self.affinity = affinity
    312         self.expertise = expertise
    313         self.pictures = {
    314             "icons_small" : None,
    315             "icons_large" : None,
    316             "portrait" : None
    317         }
    318         self.load_pictures(pictures)
    319         self.passable = passable
    320         self.normal_stats = normal_stats
    321         self.active_stats = { i : 0 for i in self.normal_stats }
    322         self.equip_mod = { i : 0 for i in self.normal_stats } # derived later
    323         self.effect_mod = { i : 0 for i in self.normal_stats } # this mod can only be applied in battle, defaults empty always
    324         self.dist_mod = { i : stat_dist[i] if i in stat_dist else 0 for i in self.normal_stats }
    325         self.other_mod = { i : stat_mod[i] if i in stat_mod else 0 for i in self.normal_stats }
    326         self.hp_damage_mod = 0
    327         self.team = TEAMS[team]
    328         self.equipment = {
    329             "weapon" : None,
    330             "armor" : None,
    331             "acc1" : None,
    332             "acc2" : None,
    333             "acc3" : None
    334         }
    335         self.growth = growth
    336         self.attack_range = None
    337         self.equip([equipment[e] for e in equipment])
    338         if level != 1:
    339             self.level_up(level)
    340         self.modulate_stats()
    341         self.back_to_stand = False # TODO: This may not be the best way
    342         self.current_tile_path = []
    343         self.current_tile_path_index = 0
    344         self.through_tile_pos = (-1, -1)
    345         self.path_moving = False
    346         self.first_path_move_pass = False
    347         self.attacking = False
    348         self.attack_target = None
    349         self.attack_anim_timer = 0
    350         self.being_damaged = False
    351         self.dodging = False
    352         self.damage_timer = 0
    353         self.damage_to_receive = 0
    354         self.damage_number_font = pygame.font.Font(os.path.join(FONT_PATH, UI_FONT), self.rect.height // 2)
    355         self.damage_number_outline_font = pygame.font.Font(os.path.join(FONT_PATH, UI_FONT), (self.rect.height // 2) + 6)
    356         self.damage_notation_font = pygame.font.Font(os.path.join(FONT_PATH, UI_FONT), self.rect.height // 3)
    357         self.damage_number_surface = None
    358         self.damage_number_outline = None
    359         self.damage_number_rect = None
    360         self.damage_number_outline_rect = None
    361         self.damage_notations = []
    362         self.damage_notation_surfaces = []
    363         self.damage_notation_rects = []
    364         self.has_moved = False
    365         self.has_acted = False
    366         self.has_guarded = False
    367         self.guarding = False
    368         self.show_guard_timer = 20
    369         self.health_bar = None
    370         self.health_bar_fill = None
    371         self.health_bar_rect = None
    372         self.health_bar_fill_rect = None
    373         self.facing_arrow = None
    374         self.create_health_bar()
    375 
    376         # Turn order values
    377         self.readiness = self.active_stats["INIT"] + self.active_stats["SPD"]
    378         self.taking_turn = False
    379         self.haste_mod = 3 # 3 - normal, 1 - minimum (slow 2), 5 - maximum (haste 2)
    380         
    381         self.set_animation(self.sheet.animations["stand_" + self.facing.name], True)
    382         
    383     def load_pictures(self, pictures):
    384         """
    385         Load universal pictures from a given pictures
    386         directory. These are just strings, not images.
    387         """
    388         self.pictures["icons_small"] = pictures + "_icons_small"
    389         self.pictures["battle_portrait"] = pictures + "_battle_portrait"
    390     
    391     def equip(self, equipment):
    392         """
    393         Assign equipment to this unit. 'equipment' is either a
    394         single string or an iterable (list, tuple) of strings
    395         that match entries in the item database.
    396         """
    397         if type(equipment) == str:
    398             if (ITEMBASE[equipment]["slot"] != "weapon" or ITEMBASE[equipment]["type"] in self.expertise):
    399                 self.equipment[ITEMBASE[equipment]["slot"]] = equipment
    400             else:
    401                 print("Failed to equip " + equipment + " to " + self.name)
    402         elif type(equipment) in (list, tuple):
    403             for e in equipment:
    404                 if e != None:
    405                     if ITEMBASE[e]["slot"] != "weapon" or ITEMBASE[e]["type"] in self.expertise:
    406                         self.equipment[ITEMBASE[e]["slot"]] = e
    407                     else:
    408                         print("Failed to equip " + e + " to " + self.name)
    409         self.derive_range()
    410 
    411     def unequip(self, slot):
    412         """
    413         Unequip items from one or more slots. 'slot' is either
    414         a single string corresponding to an equipment slot, or
    415         an iterable (list, tuple) of such strings.
    416         """
    417         if type(slot) == str:
    418             self.equipment[slot] = None
    419         elif type(slot) in (list, tuple):
    420             for s in slot:
    421                 self.equipment[s] = None
    422         self.derive_range()
    423 
    424     def derive_range(self):
    425         """
    426         Figure out this piece's range based on equipment.
    427         """
    428         if self.equipment["weapon"] != None:
    429             self.attack_range = ITEMBASE[self.equipment["weapon"]]["range"]
    430         else:
    431             self.attack_range = 0
    432 
    433     def level_up(self, level_num):
    434         """
    435         Increase the level of this piece by 'level_num'
    436         and take growth into account.
    437         """
    438         for l in range(self.level, min(self.level + level_num - 1, 21)):
    439             self.level += 1
    440             for g in self.growth:
    441                 if self.level % int(g) == 0:
    442                     for s in self.growth[g]:
    443                         self.normal_stats[s] += self.growth[g][s]
    444             self.normal_stats["HP"] += self.normal_stats["DEF"] // 3
    445 
    446     def render_damage_number(self, is_hit = True):
    447         """
    448         Render the name font image for display. Also
    449         renders damage notations.
    450         """
    451         if is_hit:
    452             self.damage_number_surface = self.damage_number_font.render(str(self.damage_to_receive), False, (255, 255, 255)).convert()
    453             self.damage_number_outline = self.damage_number_outline_font.render(str(self.damage_to_receive), False, (0, 0, 0)).convert()
    454             self.damage_number_rect = self.damage_number_surface.get_rect()
    455             self.damage_number_outline_rect = self.damage_number_outline.get_rect()
    456         else:
    457             self.damage_number_surface = None
    458             self.damage_number_outline = None
    459             self.damage_number_rect = None
    460             self.damage_number_outline_rect = None
    461         for n in self.damage_notations:
    462             ns = self.damage_notation_font.render(n[0], False, n[1]).convert()
    463             nr = ns.get_rect()
    464             self.damage_notation_surfaces.append(ns)
    465             self.damage_notation_rects.append(nr)
    466 
    467     def set_facing(self, direction):
    468         """
    469         Set the direction this piece should face. 'direction' 
    470         should be a valid enum value matching FACE_DIR.
    471         """
    472         self.facing = direction
    473 
    474     def set_move_along_tile_path(self, tile_seq):
    475         """
    476         Move along a sequence of tiles in order.
    477         """
    478         self.current_tile_path = tile_seq
    479         self.current_tile_path_index = 0
    480         self.path_moving = True
    481         self.first_path_move_pass = True
    482         self.through_tile_pos = self.tile_pos
    483 
    484     def set_attack_action(self, target):
    485         """
    486         Setup and start an attack animation and the
    487         corresponding damage calculations.
    488         """
    489         # Set facing
    490         diff = (self.tile_pos[0] - target.tile_pos[0], self.tile_pos[1] - target.tile_pos[1])
    491         if abs(diff[0]) > abs(diff[1]):
    492             if diff[0] >= 0:
    493                 self.facing = FACE_DIR.L
    494             else:
    495                 self.facing = FACE_DIR.R
    496         else:
    497             if diff[1] >= 0:
    498                 self.facing = FACE_DIR.U
    499             else:
    500                 self.facing = FACE_DIR.D
    501 
    502         # Setup the animation
    503         self.set_animation(self.sheet.animations["attack_" + self.facing.name], True)
    504 
    505         # Then, setup attack execution values
    506         self.attacking = True
    507         self.attack_target = target
    508         self.attack_anim_timer = 90
    509 
    510         # Tell the other guy to be damaged
    511         # Affinities
    512         aff = None
    513         if ITEMBASE[self.equipment["weapon"]]["type"] in ("Staff", "Book", "Wand"):
    514             aff = self.affinity
    515         # Attack notations
    516         notelist = []
    517         # Bonus for back attack
    518         ba = 0
    519         if self.facing == self.attack_target.facing:
    520             ba = 0.25
    521             notelist.append(ATTACK_NOTATIONS.backattack)
    522         # Critical chance calc
    523         ca = 1
    524         if random.randint(0, 100) in range(0, self.active_stats["LUK"]):
    525             ca = 2.5
    526             notelist.append(ATTACK_NOTATIONS.critical)
    527         prim = self.active_stats[WEAPON_TYPE_SOURCES[ITEMBASE[self.equipment["weapon"]]["type"]]]
    528         bon = WEAPON_TYPE_SOURCES[ITEMBASE[self.equipment["weapon"]]["type"]]
    529         raw_dam = max(round(((prim + (prim * ba)) * 1.6)  + random.randint(-(self.active_stats["LUK"] // 2), self.active_stats["LUK"]) // 2), 0)
    530         if bon != None and raw_dam > 0:
    531             raw_dam += round((self.active_stats[bon] + random.randint(0, self.active_stats["LUK"] // 2)) * 0.2)
    532         elif raw_dam < 0:
    533             raw_dam = 0
    534         self.attack_target.set_be_damaged_action(round(raw_dam * ca), aff, self.active_stats["ACC"] + (self.active_stats["LUK"] // 2), notelist)
    535 
    536     def set_be_damaged_action(self, raw_damage, affinity, accuracy, notations):
    537         """
    538         Setup the action of being damaged. 'affinity'
    539         is the elemental affinity of the attack. It is
    540         figured into damage calclulation here in the
    541         defensive portion of the action. The 'notations'
    542         var is a list of enum vals that are parsed to
    543         indicate special things that caused the amount
    544         of damage to change. Certain notations can also
    545         indicate that the colors of the damage numbers
    546         should change.
    547         """
    548         # First, calculate based on affinity
    549         affmod = 1
    550         if affinity != None:
    551             if affinity == AFFINITY_RELATIONS[self.affinity]["Opposite"]:
    552                 affmod = 1.5
    553                 notations.append(ATTACK_NOTATIONS.opposite)
    554             elif affinity == AFFINITY_RELATIONS[self.affinity]["Weak"]:
    555                 affmod = 2.5
    556                 notations.append(ATTACK_NOTATIONS.weakness)
    557             elif affinity == AFFINITY_RELATIONS[self.affinity]["Strong"]:
    558                 affmod = 0.5
    559                 notations.append(ATTACK_NOTATIONS.resist)
    560 
    561         # Next, see if we are hit at all
    562         cth = min(((round(accuracy * 1.5) / ((self.active_stats["SPD"] * 2) + self.active_stats["LUK"])) * 100), 95)
    563         hit = True
    564         #print(cth)
    565         if not (True if random.randint(0, 100) <= cth else False):
    566             notations = [ATTACK_NOTATIONS.miss]
    567             raw_damage = 0
    568             hit = False
    569 
    570         # Next, figure in guarding
    571         guard_factor = 1
    572         if self.guarding:
    573             guard_factor = 0.5
    574 
    575         # Next, notation handling
    576         for n in notations:
    577             if n == ATTACK_NOTATIONS.backattack:
    578                 self.damage_notations.append(("Back Attack!", (200, 0, 30)))
    579             elif n == ATTACK_NOTATIONS.critical:
    580                 self.damage_notations.append(("Critical!", (230, 230, 0)))
    581             elif n == ATTACK_NOTATIONS.opposite:
    582                 self.damage_notations.append(("Conflict!", (255, 255, 255)))
    583             elif n == ATTACK_NOTATIONS.weakness:
    584                 self.damage_notations.append(("Weak!", (200, 0, 240)))
    585             elif n == ATTACK_NOTATIONS.resist:
    586                 self.damage_notations.append(("Resist!", (50, 50, 50)))
    587             elif n == ATTACK_NOTATIONS.miss:
    588                 self.damage_notations.append(("Miss!", (255, 255, 255)))
    589 
    590         # Lastly, go through with the damage
    591         if raw_damage <= 0:
    592             self.damage_to_receive = 0
    593         else:
    594             self.damage_to_receive = max(0, round((round((raw_damage - (self.active_stats["DEF"] * 1.4)) + random.randint(-(self.active_stats["LUK"] // 2), self.active_stats["LUK"])) * affmod) * guard_factor))
    595         self.damage_timer = 100
    596         if hit:
    597             self.being_damaged = True
    598         else:
    599             self.dodging = True
    600         self.render_damage_number(hit)
    601 
    602     def set_guard_action(self):
    603         """
    604         Setup for the guarding action.
    605         """
    606         self.guarding = True
    607         self.set_animation(self.sheet.animations["block_" + self.facing.name], True)
    608 
    609     def get_full_stat_def(self):
    610         """
    611         Returns a stat_def dict organized for status
    612         display, or None if no stats are found.
    613         """
    614         if self.active_stats != None and self.normal_stats != None:
    615             return { 
    616                 "name" : self.name,
    617                 "class" : self.classname,
    618                 "level" : self.level,
    619                 "exp" : self.exp,
    620                 "rank" : self.rank,
    621                 "affinity" : self.affinity,
    622                 "expertise" : self.expertise,
    623                 "normal_stats" : self.normal_stats, 
    624                 "active_stats" : self.active_stats,
    625                 "equipment" : self.equipment,
    626                 "pictures" : self.pictures
    627             }
    628         else:
    629             return None
    630 
    631     def create_health_bar(self):
    632         """
    633         Create a health bar to be displayed along with this piece
    634         on the board. The bar is green on red, with the green
    635         percentage determined by the remaining HP percentage.
    636         """
    637         self.health_bar = pygame.Surface((TILE_WIDTH // 2, TILE_HEIGHT // 10)).convert()
    638         self.health_bar_rect = self.health_bar.get_rect()
    639         self.health_bar_fill = pygame.Surface((round((self.active_stats["HP"] / self.normal_stats["HP"]) * (self.health_bar_rect.width - 2)), 
    640                                               self.health_bar_rect.height - 2)).convert()
    641         self.health_bar_fill_rect = self.health_bar_fill.get_rect()
    642         self.health_bar.fill((45, 45, 45))
    643         self.health_bar_fill.fill((0, 200, 0))
    644 
    645     def create_facing_arrow(self):
    646         """
    647         Create a facing arrow direction indicator
    648         for the piece's sprite.
    649         """
    650         sheet = self.manager.bus.fetch("sheet_manager", "sheets")["facing_arrows_1"]
    651         if self.facing == FACE_DIR.L:
    652             self.facing_arrow = entity.Entity(sheet, (0, 0))
    653         elif self.facing == FACE_DIR.R:
    654             self.facing_arrow = entity.Entity(sheet, (0, 1))
    655         elif self.facing == FACE_DIR.U:
    656             self.facing_arrow = entity.Entity(sheet, (1, 0))
    657         elif self.facing == FACE_DIR.D:
    658             self.facing_arrow = entity.Entity(sheet, (1, 1))
    659         self.facing_arrow.rect.topleft = self.rect.topleft
    660 
    661     def execute_tile_path_move(self):
    662         """
    663         Execute a move along a tile path.
    664         """
    665         if self.current_tile_path != None and self.path_moving:
    666             if self.motion == {} and self.current_tile_path_index < len(self.current_tile_path):
    667 
    668                 # Decide facing:
    669                 face_diff = (self.through_tile_pos[0] - self.current_tile_path[self.current_tile_path_index][0],
    670                              self.through_tile_pos[1] - self.current_tile_path[self.current_tile_path_index][1])
    671                 oldface = self.facing
    672                 if face_diff == (1, 0):
    673                     self.facing = FACE_DIR.L
    674                 elif face_diff == (-1, 0):
    675                     self.facing = FACE_DIR.R
    676                 elif face_diff == (0, 1):
    677                     self.facing = FACE_DIR.U
    678                 elif face_diff == (0, -1):
    679                     self.facing = FACE_DIR.D
    680 
    681                 if self.facing != oldface or self.first_path_move_pass:
    682                     self.set_animation(self.sheet.animations["walk_" + self.facing.name], True)
    683                     self.first_path_move_pass = False
    684 
    685                 # Setup motion to next tile in the path
    686                 next_tar = ((self.current_tile_path[self.current_tile_path_index][0] * TILE_WIDTH) + (TILE_WIDTH / 2), 
    687                             (self.current_tile_path[self.current_tile_path_index][1] * TILE_HEIGHT) + (TILE_HEIGHT / 2))
    688                 self.set_motion(next_tar, PIECE_MOVE_SPEED)
    689                 self.through_tile_pos = self.current_tile_path[self.current_tile_path_index]
    690                 self.current_tile_path_index += 1
    691 
    692             elif self.motion == {}:
    693                 self.tile_pos = self.current_tile_path[self.current_tile_path_index - 1]
    694                 self.through_tile_pos = (-1, -1)
    695                 self.path_moving = False
    696                 self.current_tile_path = []
    697                 self.back_to_stand = True
    698                 self.has_moved = True
    699 
    700     def execute_guard_action(self):
    701         """
    702         Execute a guard action and pass turn.
    703         """
    704         if self.guarding and not self.has_guarded:
    705             if self.show_guard_timer > 0:
    706                 self.show_guard_timer -= 1
    707             else:
    708                 self.has_acted = True
    709                 self.has_guarded = True
    710                 self.show_guard_timer = 20
    711 
    712     def execute_attack_action(self):
    713         """
    714         Execute an attack action against a preset target.
    715         """
    716         if self.attacking:
    717             if self.attack_anim_timer > 0:
    718                 self.attack_anim_timer -= 1
    719             else:
    720                 self.attacking = False
    721                 self.has_acted = True
    722                 self.set_animation(self.sheet.animations["stand_" + self.facing.name], True)
    723 
    724     def execute_be_damaged(self):
    725         """
    726         Execute the action of being damaged.
    727         """
    728         if self.being_damaged:
    729             if self.damage_timer > 0:
    730                 self.damage_timer -= 1
    731                 # TODO: This is a bit hacky, but it looks better
    732                 if self.damage_timer == 72:
    733                     self.set_animation(self.sheet.animations["hurt_" + self.facing.name], True)
    734                 elif self.damage_timer == 30:
    735                     self.hp_damage_mod -= self.damage_to_receive
    736                     self.modulate_stats()
    737                     if self.active_stats["HP"] > 0:
    738                         self.create_health_bar()
    739             else:
    740                 self.being_damaged = False
    741                 self.damage_to_receive = 0
    742                 self.damage_number_surface = None
    743                 self.damage_number_outline = None
    744                 self.damage_number_rect = None
    745                 self.damage_number_outline_rect = None
    746                 self.damage_notations = []
    747                 self.damage_notation_surfaces = []
    748                 self.damage_notation_rects = []
    749                 if self.active_stats["HP"] <= 0:
    750                     self.manager.kill_piece(self)
    751                     self.manager.bus.perform_turn_manager_kill_piece(self)
    752                 elif self.guarding:
    753                     self.set_animation(self.sheet.animations["block_" + self.facing.name], True)
    754                 else:
    755                     self.set_animation(self.sheet.animations["stand_" + self.facing.name], True)
    756                 self.manager.bus.perform_turn_manager_refresh_state()
    757         elif self.dodging:
    758             if self.damage_timer > 0:
    759                 self.damage_timer -= 1
    760                 if self.damage_timer == 68:
    761                     self.set_animation(self.sheet.animations["dodge_" + self.facing.name], True)
    762                 elif self.damage_timer == 60:
    763                     if self.guarding:
    764                         self.set_animation(self.sheet.animations["block_" + self.facing.name], True)
    765                     else:
    766                         self.set_animation(self.sheet.animations["stand_" + self.facing.name], True)
    767             else:
    768                 self.dodging = False
    769                 self.damage_notations = []
    770                 self.damage_notation_surfaces = []
    771                 self.damage_notation_rects = []
    772                 self.manager.bus.perform_turn_manager_refresh_state()
    773 
    774     def modulate_stats(self):
    775         """
    776         Take each stat mod dict, and apply the sum of
    777         all the mods to the normal_stats to produce the
    778         current active_stats. This calculates only
    779         the equip_mod; all other mods are determined
    780         elsewhere and taken for granted here. HP is not
    781         modifiable by any mod other than hp_damage_mod;
    782         no equipment or in-game effects (including rank
    783         distribution) can directly raise HP other than
    784         leveling up.
    785         """
    786         self.equip_mod = { i : 0 for i in self.normal_stats }
    787         for e in self.equipment:
    788             if self.equipment[e] != None:
    789                 for n in self.normal_stats:
    790                     self.equip_mod[n] += ITEMBASE[self.equipment[e]]["stats"][n]
    791 
    792         for s in self.normal_stats:
    793             if s != "HP":
    794                 self.active_stats[s] = self.normal_stats[s] + self.equip_mod[s] + self.other_mod[s] + self.dist_mod[s] + self.effect_mod[s]
    795             else:
    796                 self.active_stats[s] = self.normal_stats[s] + self.hp_damage_mod
    797                 
    798     def act(self):
    799         """
    800         Overwriting basic act mode.
    801         """
    802         self.execute_tile_path_move()
    803         self.execute_attack_action()
    804         self.execute_be_damaged()
    805         self.execute_guard_action()
    806         self.modulate_stats()
    807         # TODO: Something else should be done so that this doesn't overwrite other
    808         # legit non-walking, non-standing anims. THIS MAY NOT BE THE BEST.
    809         if self.back_to_stand:
    810             self.set_animation(self.sheet.animations["stand_" + self.facing.name], True)
    811             self.back_to_stand = False
    812 
    813     def update(self, surface = None, to_animate = True):
    814         """
    815         Overwrite to account for health bars.
    816         """
    817         super().update(surface, to_animate)
    818         self.create_facing_arrow()
    819         if self.health_bar != None and self.health_bar_fill != None:
    820             self.health_bar_rect.center = (self.rect.center[0], self.rect.center[1] + (TILE_HEIGHT // 5))
    821             self.health_bar_fill_rect.topleft = (self.health_bar_rect.topleft[0] + 1, self.health_bar_rect.topleft[1] + 1)
    822             surface.blit(self.health_bar, self.health_bar_rect)
    823             surface.blit(self.health_bar_fill, self.health_bar_fill_rect)
    824         if self.facing_arrow != None:
    825             self.facing_arrow.update(surface)
    826         if self.damage_timer > 0 and self.damage_timer < 55 and self.damage_number_surface != None:
    827             self.damage_number_rect.center = (self.rect.center[0], self.rect.center[1] + (self.damage_timer // 10))
    828             self.damage_number_outline_rect.center = self.damage_number_rect.center
    829             surface.blit(self.damage_number_outline, self.damage_number_outline_rect)
    830             surface.blit(self.damage_number_surface, self.damage_number_rect)
    831         for x in range(0, len(self.damage_notation_surfaces)):
    832             if self.damage_timer <= 60 - (10 * x):
    833                 self.damage_notation_rects[x].center = (self.rect.center[0], self.rect.center[1] + ((self.damage_timer // 4) - (10 * x)))
    834                 surface.blit(self.damage_notation_surfaces[x], self.damage_notation_rects[x])
    835 
    836 ##########################
    837 # Section 3 - TileCursor #
    838 ##########################
    839 
    840 class TileCursor(entity.Entity):
    841     """
    842     Object that follows the cursor to indicate selected/highlighted
    843     tiles.
    844     """
    845 
    846     def __init__(self, sheet, sprite = (0, 0), animation = None, animated = False):
    847 
    848         # Parent initialization
    849         super().__init__(sheet, sprite, animation, animated)
    850 
    851         # Entity settings
    852         self.custom_flags = "TileCursor"
    853 
    854         # DirtySprite settings
    855         self.layer = 1