#!/usr/bin/python # # battleman.py - RPG Battle Manager # # A table-top RPG battle flow manager # Tuned pretty specifically to D&D 4e for now... need a templatized system # to do anything fancier... may develop that at some point. # # future features: # * an option for passing in multiple files that contain combatant definitions import sys sys.path.append('lib/') import cPickle as pickle import argparse import cmd import os.path import battle from battle import CombatGroup from battle import Combatant import easyinput def main(): btl = battle.Battle() ### This is the pickling jar battle_pickle = None bp_io_failed = False BP_FILE = os.path.expanduser('~/.config/4etools/battleman/battle.pickle') ### # Make sure config directory exists if not os.path.exists(os.path.dirname(BP_FILE)): os.makedirs(os.path.dirname(BP_FILE)) # Get command-line args settings = parse_args() print "Welcome to 4e Battle Manager.\n" # Resume battle if needed if settings.resume: try: with open(BP_FILE, 'r') as f: btl = pickle.load(f) battle_pickle = pickle.dumps(btl) except: print "Error: Couldn't resume. Quitting to preserve our pickle." sys.exit(1) else: # hard-coding test cases for now. # Eventually, use a state-saving text file that's easy to edit, or at least copy... btl.add_group(CombatGroup("Adele", [Combatant("Adele", hp=26, pc=True, surges=8, sw=1)], 2)) btl.add_group(CombatGroup("Aristaire", [Combatant("Aristaire", hp=20, pc=True, surges=6, sw=1)], 0)) btl.add_group(CombatGroup("Foobolds", [Combatant("Foobold", hp=50), Combatant("Foobold", hp=50), Combatant("Foobold", hp=50), Combatant("Foobold", hp=50), Combatant("Foobold", hp=50)], 20)) btl.add_group(CombatGroup("Barglins", [Combatant("Barglin", hp=1), Combatant("Barglin", hp=1)], 3)) btl.add_group(CombatGroup("Orcs of Baz", [Combatant("Orc", hp=32), Combatant("Orc", hp=32)], 1)) print btl cmd_parser = CommandParser(btl, battle_pickle, BP_FILE) cmd_parser.cmdloop() class CommandParser(cmd.Cmd): """Parse the commands from the command-line.""" def __init__(self, btl, battle_pickle, BP_FILE): cmd.Cmd.__init__(self) self.btl = btl self.battle_pickle = battle_pickle self.BP_FILE = BP_FILE self.prompt = '\n> ' def postloop(self): # Re-pickle and write if changed after every query. It's cheap # and we only have to run at user-speed anyway old_bp = self.battle_pickle self.battle_pickle = pickle.dumps(btl) if old_bp != self.battle_pickle: try: with open(self.BP_FILE, 'w') as f: f.write(self.battle_pickle) except Exception: if not self.bp_io_failed: print("Warning: can't write the battle pickle. Resuming later will fail.") self.bp_io_failed = True # a def do_add(self, line): """add [N] Add the specified number of groups""" data = line.split(' ') if len(data) >= 1: num_groups = int(data[0]) else: num_groups = easyinput.input_int('number of groups') for i in range(1, num_groups+1): print "Adding group {}".format(i) self.btl.add_group(CombatGroup.from_input()) # b def do_begin(self, line): """begin Begins the battle. Rolls initiative for NPCs and prompts for PCs""" self.btl.begin() # p def do_print(self, line): """print [index] Print detailed info for combatant with index, or combatant or group with initiative""" data = line.split(' ') if len(data) >= 1: c = self.btl.get_combatant(int(data[0])) if not c: print 'Error: Invalid combatant index.' else: print c.format_full_info() else: print self.btl.format_current_group() # l def do_list(self, line): """list Lists a summary of all of the combat groups and their members""" print self.btl.format_combatants() # d def do_damage(self, line): """damage [index] [amount] Deals damage to the specified combatant""" data = line.split(' ') c = battle.do_combatant_select(self.btl, data) if not c: return if len(data) >= 2: amount = int(data[1]) else: amount = easyinput.input_int('damage') c.damage(amount) # h def do_heal(self, line): """heal [index] [amount] Heal hit points for the specified combatant""" data = line.split(' ') c = battle.do_combatant_select(self.btl, data) if not c: return if len(data) >= 2: amount = int(data[1]) else: amount = easyinput.input_int('amount') c.heal(amount) # t def do_temp(self, line): """temp [index] [amount] Add temporary hit points to the specified combatant""" data = line.split(' ') c = battle.do_combatant_select(self.btl, data) if not c: return if len(data) >= 2: amount = int(data[1]) else: amount = easyinput.input_int('amount') c.add_temp_hp(amount) # T def do_rmtemp(self, line): """rmtemp [index] [amount] Remove temporary hit points from the specified combatant""" do_stub() # s, so def do_surge(self, line): """surge [index] [heal] Combatant with index uses a healing surge. If heal is 0, don't heal the combatant""" data = line.split(' ') c = battle.do_combatant_select(self.btl, data) if not c: return heal = True if len(data) >= 2 and data[1] == '0': heal = False c.use_surge(heal) # sw def do_wind(self, line): """wind [index] Use Second Wind for combatant""" data = line.split(' ') c = battle.do_combatant_select(self.btl, data) if not c: return c.use_second_wind() # c def do_cond(self, line): """cond [index] [name] [type] [duration] [start|end] Add a temporary condition to a combatant, optionally specifying the condition name, type (s or t), duration and what phase of the combatant's turn it expires on""" data = line.split(' ') duration = None end_type = 'e' c = battle.do_combatant_select(self.btl, data) if not c: return name = easyinput.do_data_input_str(data, 1, 'condition name') ctype = easyinput.do_data_input_str(data, 2, 'condition type', default='s', show_default=True) if ctype == 't': duration = easyinput.do_data_input_int(data, 3, 'duration') end_type = easyinput.do_data_input_str(data, 4, '(s)tart|(e)nd', default='e', show_default=True) c.add_condition(name, ctype, duration, end_type) # C def do_rmcond(self, line): """rmcond [index] [condition_index] Remove a condition from a combatant early.""" data = line.split(' ') c = battle.do_combatant_select(self.btl, data) if not c: return if len(data) >= 2: index = int(data[1]) else: cond = c.choose_condition() index = None if cond: index = cond['index'] if index != None: c.remove_condition(index) # r def do_recharge(self, line): """recharge [index] [recharge_index] Use a rechargable power""" data = line.split(' ') c = battle.do_combatant_select(self.btl, data) if not c: return if len(data) >= 2: index = int(data[1]) else: r = c.choose_recharge_power() index = None if r: index = r['index'] if index != None: c.use_recharge_power(index) # w def do_wait(self, line): """wait This function is still a stub""" do_stub() # W def do_unwait(self, line): """unwait This function is still a stub""" do_stub() # x def do_sync(self, line): """sync This function is still a stub""" do_stub() def do_EOF(self, line): self.do_quit(line) # q def do_quit(self, line): """quit Exits the program. If a battle is in progress, it is temporarily saved and can be resumed by running the program with --resume next time.""" sys.exit(0) def do_stub(): print "Sorry, this is a stub function" def parse_args(): parser = argparse.ArgumentParser(description='Command-line interface to manage battle data for D&D 4e', formatter_class=argparse.RawTextHelpFormatter) parser.add_argument('--resume', '-r', action='store_true', help='Resume the battle from the last run of the program') return parser.parse_args() if __name__ == '__main__': main()