#!/usr/bin/env python3 # -*- mode: python-mode; python-indent-offset: 4; -*- # # Copyright (C) 2023 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. import sys sys.path sys.path.append('../entity') from scriptlib import SimulTemplateEntity import xml.etree.ElementTree as ET from pathlib import Path import os import glob AttackTypes = ["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. LoadTemplatesIfParent = [ "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. FilterOut = ["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? showChangedOnly = True # Sorting parameters for the "roster variety" table ComparativeSortByCav = True ComparativeSortByChamp = True ClassesUsedForSort = [ "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. AddSortingOverlay = True # This is the path to the /templates/ folder to consider. Change this for mod # support. modsFolder = Path(__file__).resolve().parents[3] / 'binaries' / 'data' / 'mods' basePath = modsFolder / 'public' / 'simulation' / 'templates' # For performance purposes, cache opened templates files. globalTemplatesList = {} sim_entity = SimulTemplateEntity(modsFolder, None) def htbout(file, balise, value): file.write("<" + balise + ">" + value + "" + balise + ">\n") def htout(file, value): file.write("
" + value + "
\n") def fastParse(template_name): """Run ET.parse() with memoising in a global table.""" if template_name in globalTemplatesList: return globalTemplatesList[template_name] parent_string = ET.parse(template_name).getroot().get("parent") globalTemplatesList[template_name] = sim_entity.load_inherited('simulation/templates/', str(template_name), ['public']) globalTemplatesList[template_name].set("parent", parent_string) return globalTemplatesList[template_name] def getParents(template_name): template_data = fastParse(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 getParents(sim_entity.get_file('simulation/templates/', parent + ".xml", 'public')): parents.add(element) return parents def ExtractValue(value): return float(value.text) if value is not None else 0.0 # This function checks that a template has the given parent. def hasParentTemplate(template_name, parentName): return any(parentName == parent + '.xml' for parent in getParents(template_name)) def CalcUnit(UnitName, existingUnit=None): if existingUnit != None: unit = existingUnit 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 = fastParse(UnitName) # 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"] = ExtractValue(Template.find("./Health/Max")) unit["BuildTime"] = ExtractValue(Template.find("./Cost/BuildTime")) unit["Cost"]["population"] = ExtractValue(Template.find("./Cost/Population")) resource_cost = Template.find("./Cost/Resources") if resource_cost is not None: for type in list(resource_cost): unit["Cost"][type.tag] = ExtractValue(type) if Template.find("./Attack/Melee") != None: unit["RepeatRate"]["Melee"] = ExtractValue(Template.find("./Attack/Melee/RepeatTime")) unit["PrepRate"]["Melee"] = ExtractValue(Template.find("./Attack/Melee/PrepareTime")) for atttype in AttackTypes: unit["Attack"]["Melee"][atttype] = ExtractValue( 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 = [] CivAg = [] if Bonus.find("Classes") != None \ and Bonus.find("Classes").text != None: Against = Bonus.find("Classes").text.split(" ") if Bonus.find("Civ") != None and Bonus.find("Civ").text != None: CivAg = Bonus.find("Civ").text.split(" ") Val = float(Bonus.find("Multiplier").text) unit["AttackBonuses"][Bonus.tag] = { "Classes": Against, "Civs": CivAg, "Multiplier": Val, } attack_restricted_classes = Template.find("./Attack/Melee/RestrictedClasses") if attack_restricted_classes is not None: newClasses = attack_restricted_classes.text.split(" ") for elem in newClasses: if elem.find("-") != -1: newClasses.pop(newClasses.index(elem)) if elem in unit["Restricted"]: unit["Restricted"].pop(newClasses.index(elem)) unit["Restricted"] += newClasses elif Template.find("./Attack/Ranged") != None: unit["Ranged"] = True unit["Range"] = ExtractValue(Template.find("./Attack/Ranged/MaxRange")) unit["Spread"] = ExtractValue(Template.find("./Attack/Ranged/Projectile/Spread")) unit["RepeatRate"]["Ranged"] = ExtractValue(Template.find("./Attack/Ranged/RepeatTime")) unit["PrepRate"]["Ranged"] = ExtractValue(Template.find("./Attack/Ranged/PrepareTime")) for atttype in AttackTypes: unit["Attack"]["Ranged"][atttype] = ExtractValue(Template.find("./Attack/Ranged/Damage/" + atttype) ) if Template.find("./Attack/Ranged/Bonuses") != None: for Bonus in Template.find("./Attack/Ranged/Bonuses"): Against = [] CivAg = [] if Bonus.find("Classes") != None \ and Bonus.find("Classes").text != None: Against = Bonus.find("Classes").text.split(" ") if Bonus.find("Civ") != None and Bonus.find("Civ").text != None: CivAg = Bonus.find("Civ").text.split(" ") Val = float(Bonus.find("Multiplier").text) unit["AttackBonuses"][Bonus.tag] = { "Classes": Against, "Civs": CivAg, "Multiplier": Val, } if Template.find("./Attack/Melee/RestrictedClasses") != None: newClasses = Template.find("./Attack/Melee/RestrictedClasses")\ .text.split(" ") for elem in newClasses: if elem.find("-") != -1: newClasses.pop(newClasses.index(elem)) if elem in unit["Restricted"]: unit["Restricted"].pop(newClasses.index(elem)) unit["Restricted"] += newClasses if Template.find("Resistance") != None: for atttype in AttackTypes: unit["Resistance"][atttype] = ExtractValue(Template.find( "./Resistance/Entity/Damage/" + atttype )) if Template.find("./UnitMotion") != None: if Template.find("./UnitMotion/WalkSpeed") != None: unit["WalkSpeed"] = ExtractValue(Template.find("./UnitMotion/WalkSpeed")) if Template.find("./Identity/VisibleClasses") != None: newClasses = Template.find("./Identity/VisibleClasses").text.split(" ") for elem in newClasses: if elem.find("-") != -1: newClasses.pop(newClasses.index(elem)) if elem in unit["Classes"]: unit["Classes"].pop(newClasses.index(elem)) unit["Classes"] += newClasses if Template.find("./Identity/Classes") != None: newClasses = Template.find("./Identity/Classes").text.split(" ") for elem in newClasses: if elem.find("-") != -1: newClasses.pop(newClasses.index(elem)) if elem in unit["Classes"]: unit["Classes"].pop(newClasses.index(elem)) unit["Classes"] += newClasses return unit def WriteUnit(Name, UnitDict): 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"])) isChanged = WriteColouredDiff(ff, diff, isChanged) # Build Time diff = +1j + (int(tp[1]["BuildTime"]) - int(templates[parent]["BuildTime"])) isChanged = WriteColouredDiff(ff, diff, isChanged) # walk speed diff = -1j + ( float(tp[1]["WalkSpeed"]) - float(templates[parent]["WalkSpeed"]) ) isChanged = WriteColouredDiff(ff, diff, isChanged) # Resistance for atype in AttackTypes: diff = -1j + ( float(tp[1]["Resistance"][atype]) - float(templates[parent]["Resistance"][atype]) ) isChanged = WriteColouredDiff(ff, diff, isChanged) # Attack types (DPS) and rate. attType = "Ranged" if tp[1]["Ranged"] == True else "Melee" if tp[1]["RepeatRate"][attType] != "0": for atype in AttackTypes: myDPS = float(tp[1]["Attack"][attType][atype]) / ( float(tp[1]["RepeatRate"][attType]) / 1000.0 ) parentDPS = float( templates[parent]["Attack"][attType][atype]) / ( float(templates[parent]["RepeatRate"][attType]) / 1000.0 ) isChanged = WriteColouredDiff( ff, -1j + (myDPS - parentDPS), isChanged ) isChanged = WriteColouredDiff( ff, -1j + ( float(tp[1]["RepeatRate"][attType]) / 1000.0 - float(templates[parent]["RepeatRate"][attType]) / 1000.0 ), isChanged, ) # range and spread if tp[1]["Ranged"] == True: isChanged = WriteColouredDiff( ff, -1j + (float(tp[1]["Range"]) - float(templates[parent]["Range"])), isChanged, ) mySpread = float(tp[1]["Spread"]) parentSpread = float(templates[parent]["Spread"]) isChanged = WriteColouredDiff( ff, +1j + (mySpread - parentSpread), isChanged ) else: ff.write("- | - | ") else: ff.write("") for rtype in Resources: isChanged = WriteColouredDiff( ff, +1j + ( float(tp[1]["Cost"][rtype]) - float(templates[parent]["Cost"][rtype]) ), isChanged, ) isChanged = WriteColouredDiff( ff, +1j + ( float(tp[1]["Cost"]["population"]) - float(templates[parent]["Cost"]["population"]) ), isChanged, ) 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("