Slightly improve templatesanalyzer/unitTables.py
[0ad.git] / source / tools / templatesanalyzer / unitTables.py
blob5fca5d816afc4682fe6dd60c28a4ed73e50316bf
1 #!/usr/bin/env python3
2 # -*- mode: python-mode; python-indent-offset: 4; -*-
4 # Copyright (C) 2023 Wildfire Games.
6 # Permission is hereby granted, free of charge, to any person obtaining a copy
7 # of this software and associated documentation files (the "Software"), to deal
8 # in the Software without restriction, including without limitation the rights
9 # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
10 # copies of the Software, and to permit persons to whom the Software is
11 # furnished to do so, subject to the following conditions:
13 # The above copyright notice and this permission notice shall be included in
14 # all copies or substantial portions of the Software.
16 # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
17 # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
18 # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
19 # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
20 # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
21 # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
22 # THE SOFTWARE.
24 import sys
25 sys.path
26 sys.path.append('../entity')
27 from scriptlib import SimulTemplateEntity
29 import xml.etree.ElementTree as ET
30 from pathlib import Path
31 import os
32 import glob
34 AttackTypes = ["Hack", "Pierce", "Crush", "Poison", "Fire"]
35 Resources = ["food", "wood", "stone", "metal"]
37 # Generic templates to load
38 # The way this works is it tries all generic templates
39 # But only loads those who have one of the following parents
40 # EG adding "template_unit.xml" will load all units.
41 LoadTemplatesIfParent = [
42 "template_unit_infantry.xml",
43 "template_unit_cavalry.xml",
44 "template_unit_champion.xml",
45 "template_unit_hero.xml",
48 # Those describe Civs to analyze.
49 # The script will load all entities that derive (to the nth degree) from one of
50 # the above templates.
51 Civs = [
52 "athen",
53 "brit",
54 "cart",
55 "gaul",
56 "iber",
57 "kush",
58 "han",
59 "mace",
60 "maur",
61 "pers",
62 "ptol",
63 "rome",
64 "sele",
65 "spart",
66 # "gaia",
69 # Remote Civ templates with those strings in their name.
70 FilterOut = ["marian", "thureophoros", "thorakites", "kardakes"]
72 # In the Civilization specific units table, do you want to only show the units
73 # that are different from the generic templates?
74 showChangedOnly = True
76 # Sorting parameters for the "roster variety" table
77 ComparativeSortByCav = True
78 ComparativeSortByChamp = True
79 ClassesUsedForSort = [
80 "Support",
81 "Pike",
82 "Spear",
83 "Sword",
84 "Archer",
85 "Javelin",
86 "Sling",
87 "Elephant",
90 # Disable if you want the more compact basic data. Enable to allow filtering and
91 # sorting in-place.
92 AddSortingOverlay = True
94 # This is the path to the /templates/ folder to consider. Change this for mod
95 # support.
96 modsFolder = Path(__file__).resolve().parents[3] / 'binaries' / 'data' / 'mods'
97 basePath = modsFolder / 'public' / 'simulation' / 'templates'
99 # For performance purposes, cache opened templates files.
100 globalTemplatesList = {}
101 sim_entity = SimulTemplateEntity(modsFolder, None)
103 def htbout(file, balise, value):
104 file.write("<" + balise + ">" + value + "</" + balise + ">\n")
107 def htout(file, value):
108 file.write("<p>" + value + "</p>\n")
111 def fastParse(template_name):
112 """Run ET.parse() with memoising in a global table."""
113 if template_name in globalTemplatesList:
114 return globalTemplatesList[template_name]
115 parent_string = ET.parse(template_name).getroot().get("parent")
116 globalTemplatesList[template_name] = sim_entity.load_inherited('simulation/templates/', str(template_name), ['public'])
117 globalTemplatesList[template_name].set("parent", parent_string)
118 return globalTemplatesList[template_name]
121 def getParents(template_name):
122 template_data = fastParse(template_name)
123 parents_string = template_data.get("parent")
124 if parents_string is None:
125 return set()
126 parents = set()
127 for parent in parents_string.split("|"):
128 parents.add(parent)
129 for element in getParents(sim_entity.get_file('simulation/templates/', parent + ".xml", 'public')):
130 parents.add(element)
132 return parents
135 def ExtractValue(value):
136 return float(value.text) if value is not None else 0.0
138 # This function checks that a template has the given parent.
139 def hasParentTemplate(template_name, parentName):
140 return any(parentName == parent + '.xml' for parent in getParents(template_name))
143 def CalcUnit(UnitName, existingUnit=None):
144 if existingUnit != None:
145 unit = existingUnit
146 else:
147 unit = {
148 "HP": 0,
149 "BuildTime": 0,
150 "Cost": {
151 "food": 0,
152 "wood": 0,
153 "stone": 0,
154 "metal": 0,
155 "population": 0,
157 "Attack": {
158 "Melee": {"Hack": 0, "Pierce": 0, "Crush": 0},
159 "Ranged": {"Hack": 0, "Pierce": 0, "Crush": 0},
161 "RepeatRate": {"Melee": "0", "Ranged": "0"},
162 "PrepRate": {"Melee": "0", "Ranged": "0"},
163 "Resistance": {"Hack": 0, "Pierce": 0, "Crush": 0},
164 "Ranged": False,
165 "Classes": [],
166 "AttackBonuses": {},
167 "Restricted": [],
168 "WalkSpeed": 0,
169 "Range": 0,
170 "Spread": 0,
171 "Civ": None,
174 Template = fastParse(UnitName)
176 # 0ad started using unit class/category prefixed to the unit name
177 # separated by |, known as mixins since A25 (rP25223)
178 # We strip these categories for now
179 # This can be used later for classification
180 unit["Parent"] = Template.get("parent").split("|")[-1] + ".xml"
181 unit["Civ"] = Template.find("./Identity/Civ").text
182 unit["HP"] = ExtractValue(Template.find("./Health/Max"))
183 unit["BuildTime"] = ExtractValue(Template.find("./Cost/BuildTime"))
184 unit["Cost"]["population"] = ExtractValue(Template.find("./Cost/Population"))
186 resource_cost = Template.find("./Cost/Resources")
187 if resource_cost is not None:
188 for type in list(resource_cost):
189 unit["Cost"][type.tag] = ExtractValue(type)
192 if Template.find("./Attack/Melee") != None:
193 unit["RepeatRate"]["Melee"] = ExtractValue(Template.find("./Attack/Melee/RepeatTime"))
194 unit["PrepRate"]["Melee"] = ExtractValue(Template.find("./Attack/Melee/PrepareTime"))
196 for atttype in AttackTypes:
197 unit["Attack"]["Melee"][atttype] = ExtractValue( Template.find("./Attack/Melee/Damage/" + atttype))
199 attack_melee_bonus = Template.find("./Attack/Melee/Bonuses")
200 if attack_melee_bonus is not None:
201 for Bonus in attack_melee_bonus:
202 Against = []
203 CivAg = []
204 if Bonus.find("Classes") != None \
205 and Bonus.find("Classes").text != None:
206 Against = Bonus.find("Classes").text.split(" ")
207 if Bonus.find("Civ") != None and Bonus.find("Civ").text != None:
208 CivAg = Bonus.find("Civ").text.split(" ")
209 Val = float(Bonus.find("Multiplier").text)
210 unit["AttackBonuses"][Bonus.tag] = {
211 "Classes": Against,
212 "Civs": CivAg,
213 "Multiplier": Val,
216 attack_restricted_classes = Template.find("./Attack/Melee/RestrictedClasses")
217 if attack_restricted_classes is not None:
218 newClasses = attack_restricted_classes.text.split(" ")
219 for elem in newClasses:
220 if elem.find("-") != -1:
221 newClasses.pop(newClasses.index(elem))
222 if elem in unit["Restricted"]:
223 unit["Restricted"].pop(newClasses.index(elem))
224 unit["Restricted"] += newClasses
226 elif Template.find("./Attack/Ranged") != None:
227 unit["Ranged"] = True
228 unit["Range"] = ExtractValue(Template.find("./Attack/Ranged/MaxRange"))
229 unit["Spread"] = ExtractValue(Template.find("./Attack/Ranged/Projectile/Spread"))
230 unit["RepeatRate"]["Ranged"] = ExtractValue(Template.find("./Attack/Ranged/RepeatTime"))
231 unit["PrepRate"]["Ranged"] = ExtractValue(Template.find("./Attack/Ranged/PrepareTime"))
233 for atttype in AttackTypes:
234 unit["Attack"]["Ranged"][atttype] = ExtractValue(Template.find("./Attack/Ranged/Damage/" + atttype) )
236 if Template.find("./Attack/Ranged/Bonuses") != None:
237 for Bonus in Template.find("./Attack/Ranged/Bonuses"):
238 Against = []
239 CivAg = []
240 if Bonus.find("Classes") != None \
241 and Bonus.find("Classes").text != None:
242 Against = Bonus.find("Classes").text.split(" ")
243 if Bonus.find("Civ") != None and Bonus.find("Civ").text != None:
244 CivAg = Bonus.find("Civ").text.split(" ")
245 Val = float(Bonus.find("Multiplier").text)
246 unit["AttackBonuses"][Bonus.tag] = {
247 "Classes": Against,
248 "Civs": CivAg,
249 "Multiplier": Val,
251 if Template.find("./Attack/Melee/RestrictedClasses") != None:
252 newClasses = Template.find("./Attack/Melee/RestrictedClasses")\
253 .text.split(" ")
254 for elem in newClasses:
255 if elem.find("-") != -1:
256 newClasses.pop(newClasses.index(elem))
257 if elem in unit["Restricted"]:
258 unit["Restricted"].pop(newClasses.index(elem))
259 unit["Restricted"] += newClasses
261 if Template.find("Resistance") != None:
262 for atttype in AttackTypes:
263 unit["Resistance"][atttype] = ExtractValue(Template.find(
264 "./Resistance/Entity/Damage/" + atttype
269 if Template.find("./UnitMotion") != None:
270 if Template.find("./UnitMotion/WalkSpeed") != None:
271 unit["WalkSpeed"] = ExtractValue(Template.find("./UnitMotion/WalkSpeed"))
273 if Template.find("./Identity/VisibleClasses") != None:
274 newClasses = Template.find("./Identity/VisibleClasses").text.split(" ")
275 for elem in newClasses:
276 if elem.find("-") != -1:
277 newClasses.pop(newClasses.index(elem))
278 if elem in unit["Classes"]:
279 unit["Classes"].pop(newClasses.index(elem))
280 unit["Classes"] += newClasses
282 if Template.find("./Identity/Classes") != None:
283 newClasses = Template.find("./Identity/Classes").text.split(" ")
284 for elem in newClasses:
285 if elem.find("-") != -1:
286 newClasses.pop(newClasses.index(elem))
287 if elem in unit["Classes"]:
288 unit["Classes"].pop(newClasses.index(elem))
289 unit["Classes"] += newClasses
291 return unit
294 def WriteUnit(Name, UnitDict):
295 ret = "<tr>"
296 ret += '<td class="Sub">' + Name + "</td>"
297 ret += "<td>" + str("%.0f" % float(UnitDict["HP"])) + "</td>"
298 ret += "<td>" + str("%.0f" % float(UnitDict["BuildTime"])) + "</td>"
299 ret += "<td>" + str("%.1f" % float(UnitDict["WalkSpeed"])) + "</td>"
301 for atype in AttackTypes:
302 PercentValue = 1.0 - (0.9 ** float(UnitDict["Resistance"][atype]))
303 ret += (
304 "<td>"
305 + str("%.0f" % float(UnitDict["Resistance"][atype]))
306 + " / "
307 + str("%.0f" % (PercentValue * 100.0))
308 + "%</td>"
311 attType = "Ranged" if UnitDict["Ranged"] == True else "Melee"
312 if UnitDict["RepeatRate"][attType] != "0":
313 for atype in AttackTypes:
314 repeatTime = float(UnitDict["RepeatRate"][attType]) / 1000.0
315 ret += (
316 "<td>"
317 + str("%.1f" % (
318 float(UnitDict["Attack"][attType][atype]) / repeatTime
319 )) + "</td>"
322 ret += (
323 "<td>"
324 + str("%.1f" % (float(UnitDict["RepeatRate"][attType]) / 1000.0))
325 + "</td>"
327 else:
328 for atype in AttackTypes:
329 ret += "<td> - </td>"
330 ret += "<td> - </td>"
332 if UnitDict["Ranged"] == True and UnitDict["Range"] > 0:
333 ret += "<td>" + str("%.1f" % float(UnitDict["Range"])) + "</td>"
334 spread = float(UnitDict["Spread"])
335 ret += "<td>" + str("%.1f" % spread) + "</td>"
336 else:
337 ret += "<td> - </td><td> - </td>"
339 for rtype in Resources:
340 ret += "<td>" + str("%.0f" %
341 float(UnitDict["Cost"][rtype])) + "</td>"
343 ret += "<td>" + str("%.0f" %
344 float(UnitDict["Cost"]["population"])) + "</td>"
346 ret += '<td style="text-align:left;">'
347 for Bonus in UnitDict["AttackBonuses"]:
348 ret += "["
349 for classe in UnitDict["AttackBonuses"][Bonus]["Classes"]:
350 ret += classe + " "
351 ret += ": %s] " % UnitDict["AttackBonuses"][Bonus]["Multiplier"]
352 ret += "</td>"
354 ret += "</tr>\n"
355 return ret
358 # Sort the templates dictionary.
359 def SortFn(A):
360 sortVal = 0
361 for classe in ClassesUsedForSort:
362 sortVal += 1
363 if classe in A[1]["Classes"]:
364 break
365 if ComparativeSortByChamp == True and A[0].find("champion") == -1:
366 sortVal -= 20
367 if ComparativeSortByCav == True and A[0].find("cavalry") == -1:
368 sortVal -= 10
369 if A[1]["Civ"] != None and A[1]["Civ"] in Civs:
370 sortVal += 100 * Civs.index(A[1]["Civ"])
371 return sortVal
374 def WriteColouredDiff(file, diff, isChanged):
375 """helper to write coloured text.
376 diff value must always be computed as a unit_spec - unit_generic.
377 A positive imaginary part represents advantageous trait.
380 def cleverParse(diff):
381 if float(diff) - int(diff) < 0.001:
382 return str(int(diff))
383 else:
384 return str("%.1f" % float(diff))
386 isAdvantageous = diff.imag > 0
387 diff = diff.real
388 if diff != 0:
389 isChanged = True
390 else:
391 # do not change its value if one parameter is not changed (yet)
392 # some other parameter might be different
393 pass
395 if diff == 0:
396 rgb_str = "200,200,200"
397 elif isAdvantageous and diff > 0:
398 rgb_str = "180,0,0"
399 elif (not isAdvantageous) and diff < 0:
400 rgb_str = "180,0,0"
401 else:
402 rgb_str = "0,150,0"
404 file.write(
405 """<td><span style="color:rgb({});">{}</span></td>
406 """.format(
407 rgb_str, cleverParse(diff)
410 return isChanged
413 def computeUnitEfficiencyDiff(TemplatesByParent, Civs):
414 efficiency_table = {}
415 for parent in TemplatesByParent:
416 for template in [template for template in TemplatesByParent[parent] if template[1]["Civ"] not in Civs]:
417 print(template)
419 TemplatesByParent[parent] = [template for template in TemplatesByParent[parent] if template[1]["Civ"] in Civs]
420 TemplatesByParent[parent].sort(key=lambda x: Civs.index(x[1]["Civ"]))
422 for tp in TemplatesByParent[parent]:
423 # HP
424 diff = -1j + (int(tp[1]["HP"]) - int(templates[parent]["HP"]))
425 efficiency_table[(parent, tp[0], "HP")] = diff
426 efficiency_table[(parent, tp[0], "HP")] = diff
428 # Build Time
429 diff = +1j + (int(tp[1]["BuildTime"]) -
430 int(templates[parent]["BuildTime"]))
431 efficiency_table[(parent, tp[0], "BuildTime")] = diff
433 # walk speed
434 diff = -1j + (
435 float(tp[1]["WalkSpeed"]) -
436 float(templates[parent]["WalkSpeed"])
438 efficiency_table[(parent, tp[0], "WalkSpeed")] = diff
440 # Resistance
441 for atype in AttackTypes:
442 diff = -1j + (
443 float(tp[1]["Resistance"][atype])
444 - float(templates[parent]["Resistance"][atype])
446 efficiency_table[(parent, tp[0], "Resistance/" + atype)] = diff
448 # Attack types (DPS) and rate.
449 attType = "Ranged" if tp[1]["Ranged"] == True else "Melee"
450 if tp[1]["RepeatRate"][attType] != "0":
451 for atype in AttackTypes:
452 myDPS = float(tp[1]["Attack"][attType][atype]) / (
453 float(tp[1]["RepeatRate"][attType]) / 1000.0
455 parentDPS = float(
456 templates[parent]["Attack"][attType][atype]) / (
457 float(templates[parent]["RepeatRate"][attType]) / 1000.0
459 diff = -1j + (myDPS - parentDPS)
460 efficiency_table[
461 (parent, tp[0], "Attack/" + attType + "/" + atype)
462 ] = diff
463 diff = -1j + (
464 float(tp[1]["RepeatRate"][attType]) / 1000.0
465 - float(templates[parent]["RepeatRate"][attType]) / 1000.0
467 efficiency_table[
468 (parent, tp[0], "Attack/" + attType + "/" + atype +
469 "/RepeatRate")
470 ] = diff
471 # range and spread
472 if tp[1]["Ranged"] == True:
473 diff = -1j + (
474 float(tp[1]["Range"]) -
475 float(templates[parent]["Range"])
477 efficiency_table[
478 (parent, tp[0], "Attack/" + attType + "/Ranged/Range")
479 ] = diff
481 diff = (float(tp[1]["Spread"]) -
482 float(templates[parent]["Spread"]))
483 efficiency_table[
484 (parent, tp[0], "Attack/" + attType + "/Ranged/Spread")
485 ] = diff
487 for rtype in Resources:
488 diff = +1j + (
489 float(tp[1]["Cost"][rtype])
490 - float(templates[parent]["Cost"][rtype])
492 efficiency_table[(parent, tp[0], "Resources/" + rtype)] = diff
494 diff = +1j + (
495 float(tp[1]["Cost"]["population"])
496 - float(templates[parent]["Cost"]["population"])
498 efficiency_table[(parent, tp[0], "Population")] = diff
500 return efficiency_table
503 def computeTemplates(LoadTemplatesIfParent):
504 """Loops over template XMLs and selectively insert into templates dict."""
505 pwd = os.getcwd()
506 os.chdir(basePath)
507 templates = {}
508 for template in list(glob.glob("template_*.xml")):
509 if os.path.isfile(template):
510 found = False
511 for possParent in LoadTemplatesIfParent:
512 if hasParentTemplate(template, possParent):
513 found = True
514 break
515 if found == True:
516 templates[template] = CalcUnit(template)
517 os.chdir(pwd)
518 return templates
521 def computeCivTemplates(template: dict, Civs: list):
522 """Load Civ specific templates"""
523 # NOTE: whether a Civ can train a certain unit is not recorded in the unit
524 # .xml files, and hence we have to get that info elsewhere, e.g. from the
525 # Civ tree. This should be delayed until this whole parser is based on the
526 # Civ tree itself.
528 # This function must always ensure that Civ unit parenthood works as
529 # intended, i.e. a unit in a Civ indeed has a 'Civ' field recording its
530 # loyalty to that Civ. Check this when upgrading this script to keep
531 # up with the game engine.
532 pwd = os.getcwd()
533 os.chdir(basePath)
535 CivTemplates = {}
537 for Civ in Civs:
538 CivTemplates[Civ] = {}
539 # Load all templates that start with that civ indicator
540 # TODO: consider adding mixin/civs here too
541 civ_list = list(glob.glob("units/" + Civ + "/*.xml"))
542 for template in civ_list:
543 if os.path.isfile(template):
545 # filter based on FilterOut
546 breakIt = False
547 for filter in FilterOut:
548 if template.find(filter) != -1:
549 breakIt = True
550 if breakIt:
551 continue
553 # filter based on loaded generic templates
554 breakIt = True
555 for possParent in LoadTemplatesIfParent:
556 if hasParentTemplate(template, possParent):
557 breakIt = False
558 break
559 if breakIt:
560 continue
562 unit = CalcUnit(template)
564 # Remove variants for now
565 if unit["Parent"].find("template_") == -1:
566 continue
568 # load template
569 CivTemplates[Civ][template] = unit
571 os.chdir(pwd)
572 return CivTemplates
575 def computeTemplatesByParent(templates: dict, Civs: list, CivTemplates: dict):
576 """Get them in the array"""
577 # Civs:list -> CivTemplates:dict -> templates:dict -> TemplatesByParent
578 TemplatesByParent = {}
579 for Civ in Civs:
580 for CivUnitTemplate in CivTemplates[Civ]:
581 parent = CivTemplates[Civ][CivUnitTemplate]["Parent"]
583 # We have the following constant equality
584 # templates[*]["Civ"] === gaia
585 # if parent in templates and templates[parent]["Civ"] == None:
586 if parent in templates:
587 if parent not in TemplatesByParent:
588 TemplatesByParent[parent] = []
589 TemplatesByParent[parent].append(
590 (CivUnitTemplate, CivTemplates[Civ][CivUnitTemplate])
593 # debug after CivTemplates are non-empty
594 return TemplatesByParent
597 ############################################################
598 ## Pre-compute all tables
599 templates = computeTemplates(LoadTemplatesIfParent)
600 CivTemplates = computeCivTemplates(templates, Civs)
601 TemplatesByParent = computeTemplatesByParent(templates, Civs, CivTemplates)
603 # Not used; use it for your own custom analysis
604 efficiencyTable = computeUnitEfficiencyDiff(
605 TemplatesByParent, Civs
609 ############################################################
610 def writeHTML():
611 """Create the HTML file"""
612 f = open(
613 os.path.realpath(__file__).replace("unitTables.py", "")
614 + "unit_summary_table.html",
615 "w",
618 f.write(
620 <!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01//EN">
621 <html>
622 <head>
623 <title>Unit Tables</title>
624 <link rel="stylesheet" href="style.css">
625 </head>
626 <body>
629 htbout(f, "h1", "Unit Summary Table")
630 f.write("\n")
632 # Write generic templates
633 htbout(f, "h2", "Units")
634 f.write(
636 <table id="genericTemplates">
637 <thead>
638 <tr>
639 <th> </th> <th>HP </th> <th>BuildTime </th> <th>Speed(walk) </th>
640 <th colspan="5">Resistance </th>
641 <th colspan="8">Attack (DPS) </th>
642 <th colspan="5">Costs </th>
643 <th>Efficient Against </th>
644 </tr>
645 <tr class="Label" style="border-bottom:1px black solid;">
646 <th> </th> <th> </th> <th> </th> <th> </th>
647 <th>H </th> <th>P </th> <th>C </th><th>P </th><th>F </th>
648 <th>H </th> <th>P </th> <th>C </th><th>P </th><th>F </th>
649 <th>Rate </th> <th>Range </th> <th>Spread (/100m) </th>
650 <th>F </th> <th>W </th> <th>S </th> <th>M </th> <th>P </th>
651 <th> </th>
652 </tr>
653 </thead>
656 for template in templates:
657 f.write(WriteUnit(template, templates[template]))
658 f.write("</table>")
660 # Write unit specialization
661 # Sort them by civ and write them in a table.
663 # TODO: pre-compute the diffs then render, filtering out the non-interesting
664 # ones
666 f.write(
668 <h2>Units Specializations
669 </h2>
671 <p class="desc">This table compares each template to its parent, showing the
672 differences between the two.
673 <br/>Note that like any table, you can copy/paste this in Excel (or Numbers or
674 ...) and sort it.
675 </p>
677 <table id="TemplateParentComp">
678 <thead>
679 <tr>
680 <th> </th> <th> </th> <th>HP </th> <th>BuildTime </th>
681 <th>Speed (/100m) </th>
682 <th colspan="5">Resistance </th>
683 <th colspan="8">Attack </th>
684 <th colspan="5">Costs </th>
685 <th>Civ </th>
686 </tr>
687 <tr class="Label" style="border-bottom:1px black solid;">
688 <th> </th> <th> </th> <th> </th> <th> </th> <th> </th>
689 <th>H </th> <th>P </th> <th>C </th><th>P </th><th>F </th>
690 <th>H </th> <th>P </th> <th>C </th><th>P </th><th>F </th>
691 <th>Rate </th> <th>Range </th> <th>Spread </th>
692 <th>F </th> <th>W </th> <th>S </th> <th>M </th> <th>P </th>
693 <th> </th>
694 </tr>
695 </thead>
698 for parent in TemplatesByParent:
699 TemplatesByParent[parent].sort(key=lambda x: Civs.index(x[1]["Civ"]))
700 for tp in TemplatesByParent[parent]:
701 isChanged = False
702 ff = open(
703 os.path.realpath(__file__).replace("unitTables.py", "") +
704 ".cache", "w"
707 ff.write("<tr>")
708 ff.write(
709 "<th style='font-size:10px'>"
710 + parent.replace(".xml", "").replace("template_", "")
711 + "</th>"
713 ff.write(
714 '<td class="Sub">'
715 + tp[0].replace(".xml", "").replace("units/", "")
716 + "</td>"
719 # HP
720 diff = -1j + (int(tp[1]["HP"]) - int(templates[parent]["HP"]))
721 isChanged = WriteColouredDiff(ff, diff, isChanged)
723 # Build Time
724 diff = +1j + (int(tp[1]["BuildTime"]) -
725 int(templates[parent]["BuildTime"]))
726 isChanged = WriteColouredDiff(ff, diff, isChanged)
728 # walk speed
729 diff = -1j + (
730 float(tp[1]["WalkSpeed"]) -
731 float(templates[parent]["WalkSpeed"])
733 isChanged = WriteColouredDiff(ff, diff, isChanged)
735 # Resistance
736 for atype in AttackTypes:
737 diff = -1j + (
738 float(tp[1]["Resistance"][atype])
739 - float(templates[parent]["Resistance"][atype])
741 isChanged = WriteColouredDiff(ff, diff, isChanged)
743 # Attack types (DPS) and rate.
744 attType = "Ranged" if tp[1]["Ranged"] == True else "Melee"
745 if tp[1]["RepeatRate"][attType] != "0":
746 for atype in AttackTypes:
747 myDPS = float(tp[1]["Attack"][attType][atype]) / (
748 float(tp[1]["RepeatRate"][attType]) / 1000.0
750 parentDPS = float(
751 templates[parent]["Attack"][attType][atype]) / (
752 float(templates[parent]["RepeatRate"][attType]) / 1000.0
754 isChanged = WriteColouredDiff(
755 ff, -1j + (myDPS - parentDPS), isChanged
757 isChanged = WriteColouredDiff(
761 float(tp[1]["RepeatRate"][attType]) / 1000.0
762 - float(templates[parent]["RepeatRate"][attType]) / 1000.0
764 isChanged,
766 # range and spread
767 if tp[1]["Ranged"] == True:
768 isChanged = WriteColouredDiff(
771 + (float(tp[1]["Range"]) -
772 float(templates[parent]["Range"])),
773 isChanged,
775 mySpread = float(tp[1]["Spread"])
776 parentSpread = float(templates[parent]["Spread"])
777 isChanged = WriteColouredDiff(
778 ff, +1j + (mySpread - parentSpread), isChanged
780 else:
781 ff.write("<td><span style='color:rgb(200,200,200);'>-</span></td><td><span style='color:rgb(200,200,200);'>-</span></td>")
782 else:
783 ff.write("<td></td><td></td><td></td><td></td><td></td><td></td>")
785 for rtype in Resources:
786 isChanged = WriteColouredDiff(
790 float(tp[1]["Cost"][rtype])
791 - float(templates[parent]["Cost"][rtype])
793 isChanged,
796 isChanged = WriteColouredDiff(
800 float(tp[1]["Cost"]["population"])
801 - float(templates[parent]["Cost"]["population"])
803 isChanged,
806 ff.write("<td>" + tp[1]["Civ"] + "</td>")
807 ff.write("</tr>\n")
809 ff.close() # to actually write into the file
810 with open(
811 os.path.realpath(__file__).replace("unitTables.py", "") +
812 ".cache", "r"
813 ) as ff:
814 unitStr = ff.read()
816 if showChangedOnly:
817 if isChanged:
818 f.write(unitStr)
819 else:
820 # print the full table if showChangedOnly is false
821 f.write(unitStr)
823 f.write("<table/>")
825 # Table of unit having or not having some units.
826 f.write(
828 <h2>Roster Variety
829 </h2>
831 <p class="desc">This table show which civilizations have units who derive from
832 each loaded generic template.
833 <br/>Grey means the civilization has no unit derived from a generic template;
834 <br/>dark green means 1 derived unit, mid-tone green means 2, bright green
835 means 3 or more.
836 <br/>The total is the total number of loaded units for this civ, which may be
837 more than the total of units inheriting from loaded templates.
838 </p>
839 <table class="CivRosterVariety">
840 <tr>
841 <th>Template </th>
844 for civ in Civs:
845 f.write('<td class="vertical-text">' + civ + "</td>\n")
846 f.write("</tr>\n")
848 sortedDict = sorted(templates.items(), key=SortFn)
850 for tp in sortedDict:
851 if tp[0] not in TemplatesByParent:
852 continue
853 f.write("<tr><td>" + tp[0] + "</td>\n")
854 for civ in Civs:
855 found = 0
856 for temp in TemplatesByParent[tp[0]]:
857 if temp[1]["Civ"] == civ:
858 found += 1
859 if found == 1:
860 f.write('<td style="background-color:rgb(0,90,0);"></td>')
861 elif found == 2:
862 f.write('<td style="background-color:rgb(0,150,0);"></td>')
863 elif found >= 3:
864 f.write('<td style="background-color:rgb(0,255,0);"></td>')
865 else:
866 f.write('<td style="background-color:rgb(200,200,200);"></td>')
867 f.write("</tr>\n")
868 f.write(
869 '<tr style="margin-top:2px;border-top:2px #aaa solid;">\
870 <th style="text-align:right; padding-right:10px;">Total:</th>\n'
872 for civ in Civs:
873 count = 0
874 for units in CivTemplates[civ]:
875 count += 1
876 f.write('<td style="text-align:center;">' + str(count) + "</td>\n")
878 f.write("</tr>\n")
880 f.write("<table/>")
882 # Add a simple script to allow filtering on sorting directly in the HTML
883 # page.
884 if AddSortingOverlay:
885 f.write(
887 <script data-config>
888 var cast = function (val) {
889 console.log(val); if (+val != val)
890 return -999999999999;
891 return +val;
895 var filtersConfig = {
896 base_path: "tablefilter/",
897 col_0: "checklist",
898 alternate_rows: true,
899 rows_counter: true,
900 btn_reset: true,
901 loader: false,
902 status_bar: false,
903 mark_active_columns: true,
904 highlight_keywords: true,
905 col_number_format: Array(22).fill("US"),
906 filters_row_index: 2,
907 headers_row_index: 1,
908 extensions: [
910 name: "sort",
911 types: ["string",
912 ...Array(6).fill("us"),
913 ...Array(6).fill("mytype"),
914 ...Array(5).fill("us"),
915 "string",
917 on_sort_loaded: function (o, sort) {
918 sort.addSortType("mytype", cast);
922 col_widths: [...Array(18).fill(null), "120px"],
925 var tf = new TableFilter('genericTemplates', filtersConfig,2);
926 tf.init();
928 var secondFiltersConfig = {
929 base_path: "tablefilter/",
930 col_0: "checklist",
931 col_19: "checklist",
932 alternate_rows: true,
933 rows_counter: true,
934 btn_reset: true,
935 loader: false,
936 status_bar: false,
937 mark_active_columns: true,
938 highlight_keywords: true,
939 col_number_format: [null, null, ...Array(17).fill("US"), null],
940 filters_row_index: 2,
941 headers_row_index: 1,
942 extensions: [
944 name: "sort",
945 types: ["string", "string",
946 ...Array(6).fill("us"),
947 ...Array(6).fill("typetwo"),
948 ...Array(5).fill("us"),
949 "string",
951 on_sort_loaded: function (o, sort) {
952 sort.addSortType("typetwo", cast);
956 col_widths: Array(20).fill(null),
960 var tf2 = new TableFilter('TemplateParentComp', secondFiltersConfig,2);
961 tf2.init();
963 </script>
967 f.write("</body>\n</html>")
970 if __name__ == "__main__":
971 writeHTML()