Created
October 27, 2017 03:11
-
-
Save baconator/6531e3346f752a77aab9bdbb1c3d93ae to your computer and use it in GitHub Desktop.
A script for repricing GURPS Character Sheet eqp files per 'After the End' (and some house rules). Mostly I was just proud of the regex.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| import xml.etree.ElementTree as etree | |
| import re | |
| from fractions import Fraction | |
| from math import floor | |
| import sys | |
| tree = etree.parse(sys.argv[1]) | |
| output_filename = sys.argv[2] | |
| root = tree.getroot() | |
| all_equipment = root.findall("equipment") | |
| # gets anything that has a ranged_weapon damage, a value, a tech_level and is in the 'Firearms' category. | |
| def get_firearms(equipment): | |
| for item in equipment: | |
| categories = [c.text for c in item.findall(".//category")] | |
| if "Firearms" not in categories: | |
| continue | |
| damage = item.find("ranged_weapon/damage") | |
| value = item.find("value") | |
| tech_level = item.find("tech_level") | |
| if damage is None or value is None or tech_level is None: | |
| continue | |
| else: | |
| yield (item, damage, value, tech_level) | |
| def get_valued(equipment): | |
| for item in equipment: | |
| value = item.find("value") | |
| tech_level = item.find("tech_level") | |
| if value is None or tech_level is None: | |
| continue | |
| else: | |
| yield (item, value, tech_level) | |
| # breaks e.g. `4d6+2 [5d] (3) cr ex` into individual values | |
| damage_parse = re.compile(r""" | |
| (?: | |
| ([0-9]+)?\s* # full dice multiplier (e.g. x in xd+5) | |
| d\s* # the 'd' between the multiplier and the modifier | |
| ([\+-]\s* # start of the modifier sub-expression (e.g. x in 5d+x). Also, +/- | |
| [0-9]+ # the modifier | |
| )?\s* # end of the modifier sub-expression | |
| )? | |
| (?:\[ # fragmentation damage can also have modifiers :'| | |
| (?: | |
| ([0-9]+)?\s* # full dice multiplier (e.g. x in xd+5) | |
| d\s* # the 'd' between the multiplier and the modifier | |
| ([\+-]\s* # start of the modifier sub-expression (e.g. x in 5d+x). Also, +/- | |
| [0-9]+ # the modifier | |
| )?\s* # end of the modifier sub-expression | |
| )? | |
| \])?\s* # done fragmentation damage | |
| (?: # start of the dr divisor sub-expression (e.g. x in 5d+3 (x)) | |
| \(([0-9\.]+)\) # dr divisor e.g. (##) | |
| )?\s* | |
| (aff|burn|burn\s*ex|cor|cr|cr\s*ex|cut|fat|imp|pi-|pi|pi\+|pi\+\+|spec\.|Spec\.|tox)?\s* # damage type | |
| """, re.VERBOSE) # TODO: shotguns | |
| # gets all specializations and returns a tuple of (name, specialization, default modifier) | |
| def get_specializations(firearm_elem): | |
| defaults = firearm_elem.findall(".//default") | |
| for default in defaults: | |
| specialization = default.find("specialization") | |
| modifier = default.find("modifier") | |
| name = default.find("name") | |
| if specialization is None or modifier is None or name is None: | |
| continue | |
| else: | |
| yield (name.text, specialization.text, int(modifier.text)) | |
| def is_shotgun(firearm_elem): | |
| return ("Guns", "Shotgun", 0) in get_specializations(firearm_elem) | |
| def is_law(firearm_elem): | |
| return ("Guns", "LAW", 0) in get_specializations(firearm_elem) | |
| def is_grenade_launcher(firearm_elem): | |
| return ("Guns", "Grenade Launcher", 0) in get_specializations(firearm_elem) | |
| def is_gun(firearm_elem): | |
| for (name, _, _) in get_specializations(firearm_elem): | |
| if name == "Guns": | |
| return True | |
| return False | |
| # formats damage values nicely for the character builder | |
| def format_damage(full_dice, modifier, frag_full_dice, frag_modifier, armour_divisor, damage_type): | |
| output = "" | |
| if full_dice is not None: | |
| output += "{}d".format(full_dice) | |
| if modifier is not None: | |
| if modifier < 0: | |
| output += "{} ".format(modifier) | |
| elif modifier > 0: | |
| output += "+{} ".format(modifier) | |
| else: | |
| output += " " | |
| if frag_full_dice is not None: | |
| output += "[{}d".format(frag_full_dice) | |
| if frag_modifier is not None: | |
| if frag_modifier < 0: | |
| output += "{}".format(frag_modifier) | |
| elif frag_modifier > 0: | |
| output += "+{}".format(frag_modifier) | |
| output += "] " | |
| if armour_divisor is not None and not armour_divisor == 0 and not armour_divisor == 1: | |
| output += "({}) ".format(armour_divisor) | |
| if damage_type is not None: | |
| output += damage_type | |
| return output.strip() | |
| for (item_elem, value_elem, tech_level_elem) in get_valued(all_equipment): | |
| try: | |
| # Calculate price multiplier | |
| tech_level = Fraction(tech_level_elem.text) | |
| price_multiplier = 0 | |
| if 0 <= tech_level and tech_level <= 4: | |
| price_multiplier = 1 | |
| else: | |
| price_multiplier = 2**(tech_level-4) | |
| # Calculate new price | |
| value = Fraction(value_elem.text) | |
| new_value = value * price_multiplier | |
| value_elem.text = str(new_value) | |
| except ValueError as e: | |
| print("Encountered error updating price of {}.".format(item_elem.find("description").text)) | |
| for (firearm_elem, damage_elem, value_elem, tech_level_elem) in get_firearms(all_equipment): | |
| # Filter out shotguns | |
| if not is_gun(firearm_elem) or is_shotgun(firearm_elem) or is_law(firearm_elem) or is_grenade_launcher(firearm_elem): | |
| continue | |
| (full_dice, modifier, frag_full_dice, frag_modifier, armour_divisor, damage_type) = damage_parse.match(damage_elem.text).groups() | |
| if frag_full_dice is not None or frag_modifier is not None: | |
| continue | |
| if full_dice is not None: | |
| if modifier is None: | |
| modifier = 0 | |
| new_average = (max((Fraction(full_dice)*Fraction("3.5") + Fraction(modifier)), 0)/Fraction("7")) | |
| new_average_fraction = abs(new_average - floor(new_average)) | |
| new_full_dice = floor(new_average) | |
| new_modifier = 0 | |
| if new_average_fraction <= (Fraction("1/3")): | |
| new_modifier = 1 | |
| elif new_average_fraction <= (Fraction("2/3")): | |
| new_modifier = 2 | |
| else: | |
| new_modifier = -1 | |
| new_full_dice += 1 | |
| if new_full_dice == 0: | |
| new_full_dice = 1 | |
| if new_modifier == 1: | |
| new_modifier = -3 | |
| elif new_modifier == 2: | |
| new_modifier = -2 | |
| else: | |
| raise Exception() | |
| new_armour_divisor = 2 | |
| if armour_divisor is not None: | |
| new_armour_divisor = Fraction(armour_divisor)*2 | |
| damage_elem.text = format_damage(new_full_dice, new_modifier, frag_full_dice, frag_modifier, new_armour_divisor, damage_type) | |
| with open(output_filename, "wb") as output_file: | |
| tree.write(output_file) |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment