Skip to content

Instantly share code, notes, and snippets.

@baconator
Created October 27, 2017 03:11
Show Gist options
  • Select an option

  • Save baconator/6531e3346f752a77aab9bdbb1c3d93ae to your computer and use it in GitHub Desktop.

Select an option

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.
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