Return to repo list

heart-of-gold

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

commit 801a33946447c01a3cd1c05debd4cef6e127f90e
parent 88e21c6a164dc270e7822af4df3f49a527be4b59
Author: Erik Letson <hmagellan@hmagellan.com>
Date:   Tue, 17 Nov 2020 15:52:46 -0600

Added items and attack highlighting

Diffstat:
Mdata/board/testmap1/testmap1.json | 32++++++++++++++++++++++++--------
Mdata/img/board_overlays_1.png | 0
Adata/json/items.json | 56++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Msrc/board.py | 42++++++++++++++++++++++++++++++++++++++++++
Msrc/bus.py | 2++
Msrc/constants.py | 2+-
Msrc/game.py | 1-
Msrc/interface.py | 6+++++-
Msrc/piece.py | 55++++++++++++++++++++++++++++++++++++++++++++++++++++++-
Msrc/turn.py | 8++++++--
10 files changed, 190 insertions(+), 14 deletions(-)

diff --git a/data/board/testmap1/testmap1.json b/data/board/testmap1/testmap1.json @@ -13,6 +13,7 @@ "team" : "Player", "normal_stats" : { "LVL" : 1, + "EXP" : 0, "HP" : 100, "ATK" : 10, "DEF" : 6, @@ -28,10 +29,12 @@ "MOVE" : 6, "SKIL" : 3, "PASS" : 3, - "AFFINITY" : "Fire" + "AFFINITY" : "Fire", + "EXPERTISE" : ["Sword"] }, "active_stats" : { "LVL" : 1, + "EXP" : 0, "HP" : 80, "ATK" : 10, "DEF" : 6, @@ -47,7 +50,8 @@ "MOVE" : 6, "SKIL" : 3, "PASS" : 3, - "AFFINITY" : "Fire" + "AFFINITY" : "Fire", + "EXPERTISE" : ["Sword"] }, "equipment" : { "weapon" : "Basic_Sword_1", @@ -71,6 +75,7 @@ "team" : "Enemy", "normal_stats" : { "LVL" : 1, + "EXP" : 100, "HP" : 100, "ATK" : 10, "DEF" : 6, @@ -86,10 +91,12 @@ "MOVE" : 6, "SKIL" : 3, "PASS" : 3, - "AFFINITY" : "Water" + "AFFINITY" : "Water", + "EXPERTISE" : ["Sword"] }, "active_stats" : { "LVL" : 1, + "EXP" : 100, "HP" : 100, "ATK" : 10, "DEF" : 6, @@ -105,7 +112,8 @@ "MOVE" : 6, "SKIL" : 3, "PASS" : 3, - "AFFINITY" : "Water" + "AFFINITY" : "Water", + "EXPERTISE" : ["Sword"] }, "equipment" : { "weapon" : "Basic_Sword_1", @@ -129,6 +137,7 @@ "team" : "Enemy", "normal_stats" : { "LVL" : 1, + "EXP" : 0, "HP" : 100, "ATK" : 10, "DEF" : 6, @@ -144,10 +153,12 @@ "MOVE" : 6, "SKIL" : 3, "PASS" : 3, - "AFFINITY" : "Water" + "AFFINITY" : "Water", + "EXPERTISE" : ["Sword"] }, "active_stats" : { "LVL" : 1, + "EXP" : 0, "HP" : 100, "ATK" : 10, "DEF" : 6, @@ -163,7 +174,8 @@ "MOVE" : 6, "SKIL" : 3, "PASS" : 3, - "AFFINITY" : "Water" + "AFFINITY" : "Water", + "EXPERTISE" : ["Sword"] }, "equipment" : { "weapon" : "Basic_Sword_1", @@ -187,6 +199,7 @@ "team" : "Enemy", "normal_stats" : { "LVL" : 1, + "EXP" : 0, "HP" : 100, "ATK" : 10, "DEF" : 6, @@ -202,10 +215,12 @@ "MOVE" : 6, "SKIL" : 3, "PASS" : 3, - "AFFINITY" : "Water" + "AFFINITY" : "Water", + "EXPERTISE" : ["Sword"] }, "active_stats" : { "LVL" : 1, + "EXP" : 0, "HP" : 10, "ATK" : 10, "DEF" : 6, @@ -221,7 +236,8 @@ "MOVE" : 6, "SKIL" : 3, "PASS" : 3, - "AFFINITY" : "Water" + "AFFINITY" : "Water", + "EXPERTISE" : ["Sword"] }, "equipment" : { "weapon" : "Basic_Sword_1", diff --git a/data/img/board_overlays_1.png b/data/img/board_overlays_1.png Binary files differ. diff --git a/data/json/items.json b/data/json/items.json @@ -0,0 +1,56 @@ +{ + "Basic_Sword_1" : { + "name" : "Basic Sword", + "desc" : "A normal sword for normal people.", + "type" : "Sword", + "slot" : "weapon", + "value" : 100, + "rarity" : 1, + "range" : 1, + "stats" : { + "HP" : 0, + "ATK" : 3, + "DEF" : 0, + "SPD" : 0, + "ACC" : 0, + "INT" : 0, + "WIS" : 0, + "LUK" : 0, + "INIT" : 0, + "CNTR" : 0, + "GARD" : 0, + "PUSH" : 0, + "MOVE" : 0, + "SKIL" : 0, + "PASS" : 0 + }, + "effects" : [] + }, + "Basic_Armor_1" : { + "name" : "Basic Armor", + "desc" : "Armor so average it doesn't even warrant description", + "type" : "Armor", + "slot" : "armor", + "value" : 150, + "rarity" : 1, + "range" : 0, + "stats" : { + "HP" : 0, + "ATK" : 0, + "DEF" : 2, + "SPD" : 0, + "ACC" : 0, + "INT" : 0, + "WIS" : 0, + "LUK" : 0, + "INIT" : 0, + "CNTR" : 0, + "GARD" : 0, + "PUSH" : 0, + "MOVE" : 0, + "SKIL" : 0, + "PASS" : 0 + }, + "effects" : [] + } +} diff --git a/src/board.py b/src/board.py @@ -32,6 +32,9 @@ class BoardManager(manager.Manager): # Move values self.move_targets = {} # Keys = (x, y) of tiles, vals are a list of (x, y) tuples representing the path to be taken self.previous_moves = {} # Keys = (x, y) of tiles, vals are the (x, y) of the previous node + + # Attack values + self.attack_targets = [] # (x, y) of tiles def load_board(self, boardname): """ @@ -106,6 +109,16 @@ class BoardManager(manager.Manager): return e.custom_flags[1] return None + def get_overlay_attack_entity_at_pos(self, pos): + """ + Return (x, y) if there is a legal overlay attack entity at + 'pos', and return None otherwise. + """ + for e in self.board_overlay: + if "OverlayAttack" in e.custom_flags and e.rect.collidepoint(pos): + return e.custom_flags[1] + return None + def display_as_move_range(self, piece, tile_pos_list): """ Display a move range from a given list of tile_pos @@ -122,6 +135,21 @@ class BoardManager(manager.Manager): e.custom_flags = ("OverlayMove", t) self.board_overlay.add(e) + def display_as_attack_range(self, piece): + """ + Display an attack range from a given list of tile_pos + tuples. + """ + # Refresh overlay to begin with + self.load_overlay() + + self.create_attack_range(piece) + for t in self.attack_targets: + e = entity.Entity(self.bus.fetch("sheet_manager", "sheets")["board_overlays_1"], (0, 1)) + e.set_position((t[0] * TILE_WIDTH, t[1] * TILE_HEIGHT)) + e.custom_flags = ("OverlayAttack", t) + self.board_overlay.add(e) + def create_move_range(self, piece): """ Create a legal move range for the given piece. @@ -174,6 +202,20 @@ class BoardManager(manager.Manager): if distances[ds] <= movemax: self.move_targets[ds] = distances[ds] + def create_attack_range(self, piece): + """ + Generate a list of legal attack targets within range of + the given piece. + """ + self.attack_targets = [] + for layer in self.current_board.tmx_data.visible_layers: + if isinstance(layer, pytmx.TiledTileLayer): + for x, y, gid in layer: + mx = piece.tile_pos[0] - x + my = piece.tile_pos[1] - y + if not (mx == 0 and my == 0) and (abs(mx) + abs(my)) <= piece.attack_range: + self.attack_targets.append((x, y)) + def get_path_by_previous_moves(self, start_tile, target_tile): """ Generate a path (list of (x, y) tile_pos tuples) using diff --git a/src/bus.py b/src/bus.py @@ -116,6 +116,8 @@ class ManagerBus(subsystem.GameSubsystem): self.game.scene_manager.current_scene.continue_script() def perform_display_move_range_of_piece(self, piece): self.game.board_manager.display_as_move_range(piece, self.game.piece_manager.get_piece_max_legal_move(piece)) + def perform_display_attack_range_of_piece(self, piece): + self.game.board_manager.display_as_attack_range(piece) def perform_shift_turns(self): self.game.turn_manager.shift_turns() def perform_trigger_turn_manager_buttons_at_pos(self, pos): diff --git a/src/constants.py b/src/constants.py @@ -42,6 +42,6 @@ SCENE_JSON_PATH = os.path.join(JSON_PATH, "scenes") # Enums STATE_MODES = enum.Enum('STATE_MODES', 'Main_Menu_Mode Battle_Mode Still_Scene_Mode') -CTRL_MODES = enum.Enum('CTRL_MODES', 'No_Control Main_Menu_Normal Turn_Normal Turn_Select_Move Turn_Watch_Move Still_Scene_Normal') +CTRL_MODES = enum.Enum('CTRL_MODES', 'No_Control Main_Menu_Normal Turn_Normal Turn_Select_Move Turn_Select_Attack Turn_Watch_Move Turn_Watch_Attack Still_Scene_Normal') FACE_DIR = enum.Enum('FACE_DIR', 'U D L R') GAME_EFFECTS = enum.Enum('GAME_EFFECTS', 'ef_game_quit ef_game_switch_mode') diff --git a/src/game.py b/src/game.py @@ -132,7 +132,6 @@ class Game(object): self.sound_manager.update(None) # Next, update all the subsurfaces/game objects and draw them - # TODO: This is WIP and will change self.interface.update_interface() # State_Mode-specific actions (can be further subdivided by Control_Mode) diff --git a/src/interface.py b/src/interface.py @@ -53,7 +53,7 @@ class GameInterface(subsystem.GameSubsystem): React to a key being pressed. """ # TODO: This is irregular compared to mouseclick. Desireable??? - if self.game.control_mode == CTRL_MODES.Turn_Select_Move: + if self.game.control_mode == CTRL_MODES.Turn_Select_Move or self.game.control_mode == CTRL_MODES.Turn_Select_Attack: if event.key == pygame.K_q: self.game.control_mode = CTRL_MODES.Turn_Normal self.bus.perform_load_board_overlay() @@ -102,6 +102,10 @@ class GameInterface(subsystem.GameSubsystem): self.game.control_mode = CTRL_MODES.Turn_Watch_Move self.bus.perform_load_board_overlay() + # Selecting a target to attack + elif self.game.control_mode == CTRL_MODES.Turn_Select_Attack: + pass + # Still-scene mode behavior elif self.game.state_mode == STATE_MODES.Still_Scene_Mode: diff --git a/src/piece.py b/src/piece.py @@ -213,7 +213,15 @@ class Piece(entity.Entity): self.normal_stats = normal_stats self.active_stats = active_stats self.team = team - self.equipment = equipment + self.equipment = { + "weapon" : None, + "armor" : None, + "acc1" : None, + "acc2" : None, + "acc3" : None + } + self.attack_range = None + self.equip([equipment[e] for e in equipment]) self.back_to_stand = False # TODO: This may not be the best way self.current_tile_path = [] self.current_tile_path_index = 0 @@ -237,6 +245,51 @@ class Piece(entity.Entity): directory. """ self.pictures["icons_small"] = pictures + "_icons_small" + + def equip(self, equipment): + """ + Assign equipment to this unit. 'equipment' is either a + single string or an iterable (list, tuple) of strings + that match entries in the item database. + """ + database = json.load(open(os.path.join(JSON_PATH, "items.json"))) + if type(equipment) == str: + if (database[equipment]["slot"] != "weapon" or database[equipment]["type"] in self.active_stats["EXPERTISE"]): + self.equipment[database[equipment]["slot"]] = equipment + else: + print("Failed to equip " + equipment + " to " + self.name) + elif type(equipment) in (list, tuple): + for e in equipment: + if e != None: + if database[e]["slot"] != "weapon" or database[e]["type"] in self.active_stats["EXPERTISE"]: + self.equipment[database[e]["slot"]] = e + else: + print("Failed to equip " + e + " to " + self.name) + self.derive_range() + + def unequip(self, slot): + """ + Unequip items from one or more slots. 'slot' is either + a single string corresponding to an equipment slot, or + an iterable (list, tuple) of such strings. + """ + database = json.load(open(os.path.join(JSON_PATH, "items.json"))) + if type(slot) == str: + self.equipment[slot] = None + elif type(slot) in (list, tuple): + for s in slot: + self.equipment[s] = None + self.derive_range() + + def derive_range(self): + """ + Figure out this piece's range based on equipment. + """ + database = json.load(open(os.path.join(JSON_PATH, "items.json"))) + if self.equipment["weapon"] != None: + self.attack_range = database[self.equipment["weapon"]]["range"] + else: + self.attack_range = 0 def set_facing(self, direction): """ diff --git a/src/turn.py b/src/turn.py @@ -60,7 +60,7 @@ class TurnManager(manager.Manager): self.current_turn = 1 for b in range(0, 6): nab = ActionButton(sheets["action_buttons_1"], (0, b), None, False, self) - nab.set_position(((SCREEN_WIDTH // 8), (SCREEN_HEIGHT // 12) + (((SCREEN_HEIGHT // 14) - 4)) * b)) + nab.set_position(((SCREEN_WIDTH // 8), (SCREEN_HEIGHT // 10) + (((SCREEN_HEIGHT // 14) - 4)) * b)) self.action_buttons.append(nab) self.shift_turns() @@ -184,6 +184,10 @@ class TurnManager(manager.Manager): self.bus.perform_display_move_range_of_piece(self.current_active_piece) self.camera.snap_to_position(self.current_active_piece.rect.center) self.game.control_mode = CTRL_MODES.Turn_Select_Move + elif self.action_buttons[1].rect.collidepoint(pos) and self.action_buttons[1].clickable: + self.bus.perform_display_attack_range_of_piece(self.current_active_piece) + self.camera.snap_to_position(self.current_active_piece.rect.center) + self.game.control_mode = CTRL_MODES.Turn_Select_Attack elif self.action_buttons[5].rect.collidepoint(pos) and self.action_buttons[5].clickable: self.shift_turns() @@ -277,7 +281,7 @@ class TurnIcon(entity.Entity): self.piece_picture = entity.Entity(self.manager.bus.fetch("sheet_manager", "sheets")[piece.pictures["icons_small"]]) self.font = pygame.font.Font(os.path.join(FONT_PATH, UI_FONT), SCREEN_HEIGHT // 18) self.index = index - self.set_position(((3 * (SCREEN_WIDTH // 4)) + 4, ((2 * (SCREEN_HEIGHT // 20)) + 1) + ((SCREEN_HEIGHT // 24) * index))) + self.set_position(((3 * (SCREEN_WIDTH // 4)) + 4, ((SCREEN_HEIGHT // 10) + 1) + ((SCREEN_HEIGHT // 24) * index))) self.piece_picture.set_position(self.rect.topleft) self.clickable = False self.health_bar = None