#!/usr/bin/env python3 # # Copyright (C) 2024 Wildfire Games. # # Permission is hereby granted, free of charge, to any person obtaining a copy # of this software and associated documentation files (the "Software"), to deal # in the Software without restriction, including without limitation the rights # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell # copies of the Software, and to permit persons to whom the Software is # furnished to do so, subject to the following conditions: # # The above copyright notice and this permission notice shall be included in # all copies or substantial portions of the Software. # # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN # THE SOFTWARE. # ruff: noqa: SIM115 import glob import os import sys import xml.etree.ElementTree as ET from pathlib import Path sys.path.append("../entity") from scriptlib import SimulTemplateEntity ATTACK_TYPES = ["Hack", "Pierce", "Crush", "Poison", "Fire"] RESOURCES = ["food", "wood", "stone", "metal"] # Generic templates to load # The way this works is it tries all generic templates # But only loads those who have one of the following parents # EG adding "template_unit.xml" will load all units. LOAD_TEMPLATES_IF_PARENT = [ "template_unit_infantry.xml", "template_unit_cavalry.xml", "template_unit_champion.xml", "template_unit_hero.xml", ] # Those describe Civs to analyze. # The script will load all entities that derive (to the nth degree) from one of # the above templates. CIVS = [ "athen", "brit", "cart", "gaul", "iber", "kush", "han", "mace", "maur", "pers", "ptol", "rome", "sele", "spart", # "gaia", ] # Remote Civ templates with those strings in their name. FILTER_OUT = ["marian", "thureophoros", "thorakites", "kardakes"] # In the Civilization specific units table, do you want to only show the units # that are different from the generic templates? SHOW_CHANGED_ONLY = True # Sorting parameters for the "roster variety" table COMPARATIVE_SORT_BY_CAV = True COMPARATIVE_SORT_BY_CHAMP = True CLASSES_USED_FOR_SORT = [ "Support", "Pike", "Spear", "Sword", "Archer", "Javelin", "Sling", "Elephant", ] # Disable if you want the more compact basic data. Enable to allow filtering and # sorting in-place. ADD_SORTING_OVERLAY = True # This is the path to the /templates/ folder to consider. Change this for mod # support. mods_folder = Path(__file__).resolve().parents[3] / "binaries" / "data" / "mods" base_path = mods_folder / "public" / "simulation" / "templates" # For performance purposes, cache opened templates files. global_templates_list = {} sim_entity = SimulTemplateEntity(mods_folder, None) def htbout(file, balise, value): file.write("<" + balise + ">" + value + "" + balise + ">\n") def htout(file, value): file.write("
" + value + "
\n") def fast_parse(template_name): """Run ET.parse() with memoising in a global table.""" if template_name in global_templates_list: return global_templates_list[template_name] parent_string = ET.parse(template_name).getroot().get("parent") global_templates_list[template_name] = sim_entity.load_inherited( "simulation/templates/", str(template_name), ["public"] ) global_templates_list[template_name].set("parent", parent_string) return global_templates_list[template_name] def get_parents(template_name): template_data = fast_parse(template_name) parents_string = template_data.get("parent") if parents_string is None: return set() parents = set() for parent in parents_string.split("|"): parents.add(parent) for element in get_parents( sim_entity.get_file("simulation/templates/", parent + ".xml", "public") ): parents.add(element) return parents def extract_value(value): return float(value.text) if value is not None else 0.0 # This function checks that a template has the given parent. def has_parent_template(template_name, parent_name): return any(parent_name == parent + ".xml" for parent in get_parents(template_name)) def calc_unit(unit_name, existing_unit=None): if existing_unit is not None: unit = existing_unit else: unit = { "HP": 0, "BuildTime": 0, "Cost": { "food": 0, "wood": 0, "stone": 0, "metal": 0, "population": 0, }, "Attack": { "Melee": {"Hack": 0, "Pierce": 0, "Crush": 0}, "Ranged": {"Hack": 0, "Pierce": 0, "Crush": 0}, }, "RepeatRate": {"Melee": "0", "Ranged": "0"}, "PrepRate": {"Melee": "0", "Ranged": "0"}, "Resistance": {"Hack": 0, "Pierce": 0, "Crush": 0}, "Ranged": False, "Classes": [], "AttackBonuses": {}, "Restricted": [], "WalkSpeed": 0, "Range": 0, "Spread": 0, "Civ": None, } template = fast_parse(unit_name) # 0ad started using unit class/category prefixed to the unit name # separated by |, known as mixins since A25 (rP25223) # We strip these categories for now # This can be used later for classification unit["Parent"] = template.get("parent").split("|")[-1] + ".xml" unit["Civ"] = template.find("./Identity/Civ").text unit["HP"] = extract_value(template.find("./Health/Max")) unit["BuildTime"] = extract_value(template.find("./Cost/BuildTime")) unit["Cost"]["population"] = extract_value(template.find("./Cost/Population")) resource_cost = template.find("./Cost/Resources") if resource_cost is not None: for resource_type in list(resource_cost): unit["Cost"][resource_type.tag] = extract_value(resource_type) if template.find("./Attack/Melee") is not None: unit["RepeatRate"]["Melee"] = extract_value(template.find("./Attack/Melee/RepeatTime")) unit["PrepRate"]["Melee"] = extract_value(template.find("./Attack/Melee/PrepareTime")) for atttype in ATTACK_TYPES: unit["Attack"]["Melee"][atttype] = extract_value( template.find("./Attack/Melee/Damage/" + atttype) ) attack_melee_bonus = template.find("./Attack/Melee/Bonuses") if attack_melee_bonus is not None: for bonus in attack_melee_bonus: against = [] civ_ag = [] if bonus.find("Classes") is not None and bonus.find("Classes").text is not None: against = bonus.find("Classes").text.split(" ") if bonus.find("Civ") is not None and bonus.find("Civ").text is not None: civ_ag = bonus.find("Civ").text.split(" ") val = float(bonus.find("Multiplier").text) unit["AttackBonuses"][bonus.tag] = { "Classes": against, "Civs": civ_ag, "Multiplier": val, } attack_restricted_classes = template.find("./Attack/Melee/RestrictedClasses") if attack_restricted_classes is not None: new_classes = attack_restricted_classes.text.split(" ") for elem in new_classes: if elem.find("-") != -1: new_classes.pop(new_classes.index(elem)) if elem in unit["Restricted"]: unit["Restricted"].pop(new_classes.index(elem)) unit["Restricted"] += new_classes elif template.find("./Attack/Ranged") is not None: unit["Ranged"] = True unit["Range"] = extract_value(template.find("./Attack/Ranged/MaxRange")) unit["Spread"] = extract_value(template.find("./Attack/Ranged/Projectile/Spread")) unit["RepeatRate"]["Ranged"] = extract_value(template.find("./Attack/Ranged/RepeatTime")) unit["PrepRate"]["Ranged"] = extract_value(template.find("./Attack/Ranged/PrepareTime")) for atttype in ATTACK_TYPES: unit["Attack"]["Ranged"][atttype] = extract_value( template.find("./Attack/Ranged/Damage/" + atttype) ) if template.find("./Attack/Ranged/Bonuses") is not None: for bonus in template.find("./Attack/Ranged/Bonuses"): against = [] civ_ag = [] if bonus.find("Classes") is not None and bonus.find("Classes").text is not None: against = bonus.find("Classes").text.split(" ") if bonus.find("Civ") is not None and bonus.find("Civ").text is not None: civ_ag = bonus.find("Civ").text.split(" ") val = float(bonus.find("Multiplier").text) unit["AttackBonuses"][bonus.tag] = { "Classes": against, "Civs": civ_ag, "Multiplier": val, } if template.find("./Attack/Melee/RestrictedClasses") is not None: new_classes = template.find("./Attack/Melee/RestrictedClasses").text.split(" ") for elem in new_classes: if elem.find("-") != -1: new_classes.pop(new_classes.index(elem)) if elem in unit["Restricted"]: unit["Restricted"].pop(new_classes.index(elem)) unit["Restricted"] += new_classes if template.find("Resistance") is not None: for atttype in ATTACK_TYPES: unit["Resistance"][atttype] = extract_value( template.find("./Resistance/Entity/Damage/" + atttype) ) if ( template.find("./UnitMotion") is not None and template.find("./UnitMotion/WalkSpeed") is not None ): unit["WalkSpeed"] = extract_value(template.find("./UnitMotion/WalkSpeed")) if template.find("./Identity/VisibleClasses") is not None: new_classes = template.find("./Identity/VisibleClasses").text.split(" ") for elem in new_classes: if elem.find("-") != -1: new_classes.pop(new_classes.index(elem)) if elem in unit["Classes"]: unit["Classes"].pop(new_classes.index(elem)) unit["Classes"] += new_classes if template.find("./Identity/Classes") is not None: new_classes = template.find("./Identity/Classes").text.split(" ") for elem in new_classes: if elem.find("-") != -1: new_classes.pop(new_classes.index(elem)) if elem in unit["Classes"]: unit["Classes"].pop(new_classes.index(elem)) unit["Classes"] += new_classes return unit def write_unit(name, unit_dict): ret = "HP | BuildTime | Speed(walk) | Resistance | Attack (DPS) | Costs | Efficient Against | ||||||||||||||||
---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
H | P | C | P | F | H | P | C | P | F | Rate | Range | Spread (/100m) | F | W | S | M | P |
This table compares each template to its parent, showing the
differences between the two.
Note that like any table, you can copy/paste this in Excel (or Numbers or
...) and sort it.
HP | BuildTime | Speed (/100m) | Resistance | Attack | Costs | Civ | |||||||||||||||||
---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
H | P | C | P | F | H | P | C | P | F | Rate | Range | Spread | F | W | S | M | P | ||||||
" + parent.replace(".xml", "").replace("template_", "") + " | " ) ff.write( '' + tp[0].replace(".xml", "").replace("units/", "") + " | " ) # HP diff = -1j + (int(tp[1]["HP"]) - int(templates[parent]["HP"])) is_changed = write_coloured_diff(ff, diff, is_changed) # Build Time diff = +1j + (int(tp[1]["BuildTime"]) - int(templates[parent]["BuildTime"])) is_changed = write_coloured_diff(ff, diff, is_changed) # walk speed diff = -1j + (float(tp[1]["WalkSpeed"]) - float(templates[parent]["WalkSpeed"])) is_changed = write_coloured_diff(ff, diff, is_changed) # Resistance for atype in ATTACK_TYPES: diff = -1j + ( float(tp[1]["Resistance"][atype]) - float(templates[parent]["Resistance"][atype]) ) is_changed = write_coloured_diff(ff, diff, is_changed) # Attack types (DPS) and rate. att_type = "Ranged" if tp[1]["Ranged"] is True else "Melee" if tp[1]["RepeatRate"][att_type] != "0": for atype in ATTACK_TYPES: my_dps = float(tp[1]["Attack"][att_type][atype]) / ( float(tp[1]["RepeatRate"][att_type]) / 1000.0 ) parent_dps = float(templates[parent]["Attack"][att_type][atype]) / ( float(templates[parent]["RepeatRate"][att_type]) / 1000.0 ) is_changed = write_coloured_diff(ff, -1j + (my_dps - parent_dps), is_changed) is_changed = write_coloured_diff( ff, -1j + ( float(tp[1]["RepeatRate"][att_type]) / 1000.0 - float(templates[parent]["RepeatRate"][att_type]) / 1000.0 ), is_changed, ) # range and spread if tp[1]["Ranged"] is True: is_changed = write_coloured_diff( ff, -1j + (float(tp[1]["Range"]) - float(templates[parent]["Range"])), is_changed, ) my_spread = float(tp[1]["Spread"]) parent_spread = float(templates[parent]["Spread"]) is_changed = write_coloured_diff( ff, +1j + (my_spread - parent_spread), is_changed ) else: ff.write( "- | " "- | " ) else: ff.write("") for rtype in RESOURCES: is_changed = write_coloured_diff( ff, +1j + (float(tp[1]["Cost"][rtype]) - float(templates[parent]["Cost"][rtype])), is_changed, ) is_changed = write_coloured_diff( ff, +1j + ( float(tp[1]["Cost"]["population"]) - float(templates[parent]["Cost"]["population"]) ), is_changed, ) ff.write(" | " + tp[1]["Civ"] + " | ") ff.write("
This table show which civilizations have units who derive from
each loaded generic template.
Grey means the civilization has no unit derived from a generic template;
dark green means 1 derived unit, mid-tone green means 2, bright green
means 3 or more.
The total is the total number of loaded units for this civ, which may be
more than the total of units inheriting from loaded templates.
Template | """ ) for civ in CIVS: f.write('' + civ + " | \n") f.write("|||
---|---|---|---|---|
" + tp[0] + " | \n") for civ in CIVS: found = 0 for temp in TemplatesByParent[tp[0]]: if temp[1]["Civ"] == civ: found += 1 if found == 1: f.write('') elif found == 2: f.write(' | ') elif found >= 3: f.write(' | ') else: f.write(' | ') f.write(" |
Total: | \n' ) for civ in CIVS: count = 0 for _units in CivTemplates[civ]: count += 1 f.write('' + str(count) + " | \n") f.write("