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
26 sys
.path
.append('../entity')
27 from scriptlib
import SimulTemplateEntity
29 import xml
.etree
.ElementTree
as ET
30 from pathlib
import Path
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.
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
= [
90 # Disable if you want the more compact basic data. Enable to allow filtering and
92 AddSortingOverlay
= True
94 # This is the path to the /templates/ folder to consider. Change this for mod
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:
127 for parent
in parents_string
.split("|"):
129 for element
in getParents(sim_entity
.get_file('simulation/templates/', parent
+ ".xml", 'public')):
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:
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},
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
:
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
] = {
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"):
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
] = {
251 if Template
.find("./Attack/Melee/RestrictedClasses") != None:
252 newClasses
= Template
.find("./Attack/Melee/RestrictedClasses")\
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
294 def WriteUnit(Name
, UnitDict
):
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
]))
305 + str("%.0f" % float(UnitDict
["Resistance"][atype
]))
307 + str("%.0f" % (PercentValue
* 100.0))
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
318 float(UnitDict
["Attack"][attType
][atype
]) / repeatTime
324 + str("%.1f" % (float(UnitDict
["RepeatRate"][attType
]) / 1000.0))
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>"
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"]:
349 for classe
in UnitDict
["AttackBonuses"][Bonus
]["Classes"]:
351 ret
+= ": %s] " % UnitDict
["AttackBonuses"][Bonus
]["Multiplier"]
358 # Sort the templates dictionary.
361 for classe
in ClassesUsedForSort
:
363 if classe
in A
[1]["Classes"]:
365 if ComparativeSortByChamp
== True and A
[0].find("champion") == -1:
367 if ComparativeSortByCav
== True and A
[0].find("cavalry") == -1:
369 if A
[1]["Civ"] != None and A
[1]["Civ"] in Civs
:
370 sortVal
+= 100 * Civs
.index(A
[1]["Civ"])
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
))
384 return str("%.1f" % float(diff
))
386 isAdvantageous
= diff
.imag
> 0
391 # do not change its value if one parameter is not changed (yet)
392 # some other parameter might be different
396 rgb_str
= "200,200,200"
397 elif isAdvantageous
and diff
> 0:
399 elif (not isAdvantageous
) and diff
< 0:
405 """<td><span style="color:rgb({});">{}</span></td>
407 rgb_str
, cleverParse(diff
)
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
]:
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
]:
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
429 diff
= +1j
+ (int(tp
[1]["BuildTime"]) -
430 int(templates
[parent
]["BuildTime"]))
431 efficiency_table
[(parent
, tp
[0], "BuildTime")] = diff
435 float(tp
[1]["WalkSpeed"]) -
436 float(templates
[parent
]["WalkSpeed"])
438 efficiency_table
[(parent
, tp
[0], "WalkSpeed")] = diff
441 for atype
in AttackTypes
:
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
456 templates
[parent
]["Attack"][attType
][atype
]) / (
457 float(templates
[parent
]["RepeatRate"][attType
]) / 1000.0
459 diff
= -1j
+ (myDPS
- parentDPS
)
461 (parent
, tp
[0], "Attack/" + attType
+ "/" + atype
)
464 float(tp
[1]["RepeatRate"][attType
]) / 1000.0
465 - float(templates
[parent
]["RepeatRate"][attType
]) / 1000.0
468 (parent
, tp
[0], "Attack/" + attType
+ "/" + atype
+
472 if tp
[1]["Ranged"] == True:
474 float(tp
[1]["Range"]) -
475 float(templates
[parent
]["Range"])
478 (parent
, tp
[0], "Attack/" + attType
+ "/Ranged/Range")
481 diff
= (float(tp
[1]["Spread"]) -
482 float(templates
[parent
]["Spread"]))
484 (parent
, tp
[0], "Attack/" + attType
+ "/Ranged/Spread")
487 for rtype
in Resources
:
489 float(tp
[1]["Cost"][rtype
])
490 - float(templates
[parent
]["Cost"][rtype
])
492 efficiency_table
[(parent
, tp
[0], "Resources/" + rtype
)] = diff
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."""
508 for template
in list(glob
.glob("template_*.xml")):
509 if os
.path
.isfile(template
):
511 for possParent
in LoadTemplatesIfParent
:
512 if hasParentTemplate(template
, possParent
):
516 templates
[template
] = CalcUnit(template
)
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
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.
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
547 for filter in FilterOut
:
548 if template
.find(filter) != -1:
553 # filter based on loaded generic templates
555 for possParent
in LoadTemplatesIfParent
:
556 if hasParentTemplate(template
, possParent
):
562 unit
= CalcUnit(template
)
564 # Remove variants for now
565 if unit
["Parent"].find("template_") == -1:
569 CivTemplates
[Civ
][template
] = unit
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
= {}
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 ############################################################
611 """Create the HTML file"""
613 os
.path
.realpath(__file__
).replace("unitTables.py", "")
614 + "unit_summary_table.html",
620 <!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01//EN">
623 <title>Unit Tables</title>
624 <link rel="stylesheet" href="style.css">
629 htbout(f
, "h1", "Unit Summary Table")
632 # Write generic templates
633 htbout(f
, "h2", "Units")
636 <table id="genericTemplates">
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>
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>
656 for template
in templates
:
657 f
.write(WriteUnit(template
, templates
[template
]))
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
668 <h2>Units Specializations
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
677 <table id="TemplateParentComp">
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>
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>
698 for parent
in TemplatesByParent
:
699 TemplatesByParent
[parent
].sort(key
=lambda x
: Civs
.index(x
[1]["Civ"]))
700 for tp
in TemplatesByParent
[parent
]:
703 os
.path
.realpath(__file__
).replace("unitTables.py", "") +
709 "<th style='font-size:10px'>"
710 + parent
.replace(".xml", "").replace("template_", "")
715 + tp
[0].replace(".xml", "").replace("units/", "")
720 diff
= -1j
+ (int(tp
[1]["HP"]) - int(templates
[parent
]["HP"]))
721 isChanged
= WriteColouredDiff(ff
, diff
, isChanged
)
724 diff
= +1j
+ (int(tp
[1]["BuildTime"]) -
725 int(templates
[parent
]["BuildTime"]))
726 isChanged
= WriteColouredDiff(ff
, diff
, isChanged
)
730 float(tp
[1]["WalkSpeed"]) -
731 float(templates
[parent
]["WalkSpeed"])
733 isChanged
= WriteColouredDiff(ff
, diff
, isChanged
)
736 for atype
in AttackTypes
:
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
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
767 if tp
[1]["Ranged"] == True:
768 isChanged
= WriteColouredDiff(
771 + (float(tp
[1]["Range"]) -
772 float(templates
[parent
]["Range"])),
775 mySpread
= float(tp
[1]["Spread"])
776 parentSpread
= float(templates
[parent
]["Spread"])
777 isChanged
= WriteColouredDiff(
778 ff
, +1j
+ (mySpread
- parentSpread
), isChanged
781 ff
.write("<td><span style='color:rgb(200,200,200);'>-</span></td><td><span style='color:rgb(200,200,200);'>-</span></td>")
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
])
796 isChanged
= WriteColouredDiff(
800 float(tp
[1]["Cost"]["population"])
801 - float(templates
[parent
]["Cost"]["population"])
806 ff
.write("<td>" + tp
[1]["Civ"] + "</td>")
809 ff
.close() # to actually write into the file
811 os
.path
.realpath(__file__
).replace("unitTables.py", "") +
820 # print the full table if showChangedOnly is false
825 # Table of unit having or not having some units.
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
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.
839 <table class="CivRosterVariety">
845 f
.write('<td class="vertical-text">' + civ
+ "</td>\n")
848 sortedDict
= sorted(templates
.items(), key
=SortFn
)
850 for tp
in sortedDict
:
851 if tp
[0] not in TemplatesByParent
:
853 f
.write("<tr><td>" + tp
[0] + "</td>\n")
856 for temp
in TemplatesByParent
[tp
[0]]:
857 if temp
[1]["Civ"] == civ
:
860 f
.write('<td style="background-color:rgb(0,90,0);"></td>')
862 f
.write('<td style="background-color:rgb(0,150,0);"></td>')
864 f
.write('<td style="background-color:rgb(0,255,0);"></td>')
866 f
.write('<td style="background-color:rgb(200,200,200);"></td>')
869 '<tr style="margin-top:2px;border-top:2px #aaa solid;">\
870 <th style="text-align:right; padding-right:10px;">Total:</th>\n'
874 for units
in CivTemplates
[civ
]:
876 f
.write('<td style="text-align:center;">' + str(count
) + "</td>\n")
882 # Add a simple script to allow filtering on sorting directly in the HTML
884 if AddSortingOverlay
:
888 var cast = function (val) {
889 console.log(val); if (+val != val)
890 return -999999999999;
895 var filtersConfig = {
896 base_path: "tablefilter/",
898 alternate_rows: true,
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,
912 ...Array(6).fill("us"),
913 ...Array(6).fill("mytype"),
914 ...Array(5).fill("us"),
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);
928 var secondFiltersConfig = {
929 base_path: "tablefilter/",
932 alternate_rows: true,
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,
945 types: ["string", "string",
946 ...Array(6).fill("us"),
947 ...Array(6).fill("typetwo"),
948 ...Array(5).fill("us"),
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);
967 f
.write("</body>\n</html>")
970 if __name__
== "__main__":