wesnoth/data/tools/unit_tree/html_output.py
2025-02-17 13:17:59 -06:00

1567 lines
58 KiB
Python

#!/usr/bin/env python3
# encoding: utf-8
import copy
import gettext
import html
import os
import re
import time
import traceback
import urllib.parse
import unit_tree.helpers as helpers
import wesnoth.wmlparser3 as wmlparser3
PICS_LOCATION = os.path.join("..", "..", "pics")
# Icons for mainline terrains used on the unit details page
TERRAIN_ICONS = {
"fungus": "forest/mushrooms-tile",
"cave": "cave/floor6",
"sand": "sand/beach",
"reef": "water/reef-tropical-tile",
"hills": "hills/regular",
"swamp_water": "swamp/water-tile",
"shallow_water": "water/coast-tile",
"castle": "castle/castle-tile",
"mountains": "mountains/snow-tile",
"deep_water": "water/ocean-tile",
"flat": "grass/green-symbol",
"forest": "forest/pine-tile",
"frozen": "frozen/ice",
"village": "village/human-tile",
"impassable": "void/void",
"unwalkable": "unwalkable/lava",
"rails": "misc/rails-ne-sw",
}
# Omit these terrains from the terrain info report on the unit details page
HIDDEN_TERRAINS = [
"off_map", "off_map2", "fog", "shroud", "impassable", "void", "rails"
]
# Damage tpye ids and associated icons used on the details page
RESISTANCES = [
("blade", "attacks/sword-human.png"),
("pierce", "attacks/spear.png"),
("impact", "attacks/club.png"),
("fire", "attacks/fireball.png"),
("cold", "attacks/iceball.png"),
("arcane", "attacks/faerie-fire.png")
]
WESMERE_CSS_VERSION = "1.2.0"
WESMERE_CSS_PREFIX = "https://www.wesnoth.org"
WESMERE_HEADER = '''\
<!DOCTYPE html>
<html class="no-js %(classes)s" lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width,initial-scale=1" />
<link rel="icon" type="image/png" href="https://www.wesnoth.org/wesmere/img/favicon-32.png" sizes="32x32" />
<link rel="icon" type="image/png" href="https://www.wesnoth.org/wesmere/img/favicon-16.png" sizes="16x16" />
<link rel="stylesheet" type="text/css" href="%(cssprefix)s/wesmere/css/wesmere-%(cssver)s.css" />
<link rel="stylesheet" type="text/css" href="%(cssprefix)s/wesmere/css/wmlunits-%(cssver)s.css" />
<script src="https://www.wesnoth.org/wesmere/js/modernizr.js"></script>
<script type="text/javascript" src="%(path)s/menu.js"></script>
<title>%(title)s - Wesnoth Units Database</title>
</head>
<body>
<div id="main">
<div id="nav" role="banner">
<div class="centerbox">
<div id="logo">
<a href="https://www.wesnoth.org/" aria-label="Wesnoth logo"></a>
</div>
<ul id="navlinks">
<li><a href="https://units.wesnoth.org/">Units</a></li>
<li><a href="https://www.wesnoth.org/">Home</a></li>
<li><a href="https://forums.wesnoth.org/viewforum.php?f=62">News</a></li>
<li><a href="https://wiki.wesnoth.org/Play">Play</a></li>
<li><a href="https://wiki.wesnoth.org/Create">Create</a></li>
<li><a href="https://forums.wesnoth.org/">Forums</a></li>
<li><a href="https://wiki.wesnoth.org/Project">About</a></li>
</ul>
<div id="sitesearch" role="search">
<form method="get" action="https://wiki.wesnoth.org/">
<input id="searchbox" type="search" name="search" placeholder="Search" title="Search the wiki [Alt+Shift+f]" accesskey="f" />
<span id="searchbox-controls">
<button id="search-go" class="search-button" type="submit" title="Search">
<i class="search-icon" aria-hidden="true"></i>
<span class="sr-label">Search the wiki</span>
</button>
</span>
</form>
</div>
<div class="reset"></div>
</div>
</div>
<div id="content" role="main">'''
WESMERE_FOOTER = '''\
</div> <!-- end content -->
</div> <!-- end main -->
<div id="footer-sep"></div>
<div id="footer"><div id="footer-content"><div>
<a href="https://wiki.wesnoth.org/StartingPoints">Site Map</a> &#8226; <a href="https://status.wesnoth.org/">Site Status</a><br />
Copyright &copy; 2003&ndash;2025 by <a rel="author" href="https://wiki.wesnoth.org/Project">The Battle for Wesnoth Project</a><br />
Site design Copyright &copy; 2017&ndash;2025 by Iris Morelle
</div></div></div>
</body></html>
'''
HTML_CLEAR_FLOATS = '<div class="reset"></div>'
HTML_ENTITY_HORIZONTAL_BAR = '&#8213;'
HTML_ENTITY_MULTIPLICATION_SIGN = '&#215;'
HTML_ENTITY_FIGURE_DASH = '&#8210;'
PRE_PLACEHOLDER_CAMPAIGNS = b"PLACE CAMPAIGNS HERE\n"
PRE_PLACEHOLDER_ERAS = b"PLACE ERAS HERE\n"
def website_header(title='', path='../../', classes=[]):
"""Returns the website header with the specified parameters."""
return WESMERE_HEADER % {
"title": title,
"path": path,
"cssprefix": WESMERE_CSS_PREFIX,
"cssver": WESMERE_CSS_VERSION,
"classes": ' '.join(['wmlunits'] + classes)}
def website_footer():
"""Returns the website footer."""
return WESMERE_FOOTER
def build_timestamp():
"""Returns an element containing the current date and time."""
return '<div id="lastmod">Last updated on %s.</div>' % time.ctime()
all_written_html_files = []
error_only_once = {}
def error_message(message):
if message in error_only_once:
return
error_only_once[message] = 1
write_error(message)
helpers.error_message = error_message
def reset_errors():
error_only_once = {}
def int_fallback(str_value, int_fallback=0):
try:
return int(str_value)
except TypeError:
return int_fallback
except ValueError:
return int_fallback
def path2url(url):
if url is None:
return url
return re.sub(r'\\', '/', url)
def cleanurl(url):
"""
Encode the given URL to ensure it only contains valid URL characters
(also known as percent-encoding).
"""
if url is None:
return url
return urllib.parse.quote(url, encoding='utf-8')
def cleantext(text, quote=True):
"""Escape any HTML special characters in the given string."""
if text is None:
return text
return html.escape(text, quote)
def resistance_rating_color_class(resistance):
"""Return a color class adequate for the provided unit resistance value."""
if resistance < 0:
return 'red'
elif resistance <= 20:
return 'yellow'
elif resistance <= 40:
return 'olive'
else:
return 'green'
def defense_rating_color_class(defense):
"""Return a color class adequate for the provided terrain defense value."""
if defense <= 10:
return 'red'
elif defense <= 30:
return 'yellow'
elif defense <= 50:
return 'olive'
else:
return 'green'
def mvtcost_rating_color_class(str_mvtcost, str_moves):
"""Return a color class adequate for the provided movement cost value."""
cost = int_fallback(str_mvtcost, 99)
moves = int_fallback(str_moves)
if cost >= moves:
return 'gray'
elif cost > moves/2:
return 'red'
elif cost > 1:
return 'yellow'
else:
return 'green'
class MyFile:
"""
Python 2 is a bit weird with encodings, really should switch this to
Python 3.
"""
def __init__(self, filename, mode):
self.filename = filename
self.fileobj = open(filename, mode + "b")
def write(self, line):
self.fileobj.write(line.encode("utf8"))
def close(self):
self.fileobj.close()
class Translation:
def __init__(self, localedir, langcode):
self.catalog = {}
self.localedir = localedir
self.langcode = langcode
class Dummy:
def gettext(self, msgid):
if not msgid:
return ""
caret = msgid.find("^")
if caret < 0:
return msgid
return msgid[caret + 1:]
self.dummy = Dummy()
def translate(self, string, textdomain):
if textdomain not in self.catalog:
try:
self.catalog[textdomain] = gettext.translation(
textdomain, self.localedir, [self.langcode])
self.catalog[textdomain].add_fallback(self.dummy)
except IOError:
self.catalog[textdomain] = self.dummy
except AttributeError:
self.catalog[textdomain] = self.dummy
except IndexError:
# not sure why, but this happens within the
# gettext.translation call sometimes
self.catalog[textdomain] = self.dummy
except ValueError:
self.catalog[textdomain] = self.dummy
return self.catalog[textdomain].gettext(string)
class GroupByRace:
def __init__(self, wesnoth, campaign):
self.wesnoth = wesnoth
self.campaign = campaign
def unitfilter(self, unit):
if not self.campaign:
return True
return unit.campaigns and self.campaign == unit.campaigns[0]
def groups(self, unit):
return [T(unit.race, "plural_name")]
def group_name(self, group):
if not group:
return "None"
return group
class GroupByNothing:
def __init__(self):
pass
def unitfilter(self, unit):
return True
def groups(self, unit):
return ["units"]
def group_name(self, group):
return "units"
class GroupByFaction:
def __init__(self, wesnoth, era):
self.wesnoth = wesnoth
self.era = era
def unitfilter(self, unit):
return self.era in unit.eras
def groups(self, unit):
return [x for x in unit.factions if x[0] == self.era]
def group_name(self, group):
era = self.wesnoth.era_lookup[group[0]]
if group[1]:
faction = era.faction_lookup[group[1]]
name = T(faction, "name")
if name:
name = name[name.rfind("=") + 1:]
else:
name = "missing"
error_message("Warning: %s has no faction name\n" % group[1])
else:
name = "factionless"
return name
global_htmlout = None
def T(tag, att):
if not tag:
return "none"
return tag.get_text_val(att, translation=global_htmlout.translate)
class HTMLOutput:
def __init__(self, isocode, output, addon, campaign, is_era, wesnoth, verbose=False):
global global_htmlout
self.output = output
self.addon = addon
self.campaign = campaign
self.is_era = is_era
self.verbose = verbose
self.target = "index.html"
self.wesnoth = wesnoth
self.forest = None
self.translation = Translation(options.transdir, isocode)
self.isocode = isocode
global_htmlout = self
def translate(self, string, domain):
return self.translation.translate(string, domain)
def analyze_units(self, grouper, add_parents):
"""
This takes all units belonging to a campaign, then groups them either
by race or faction, and creates an advancements tree out of it.
"""
# Build an advancement tree forest of all units.
forest = self.forest = helpers.UnitForest()
units_added = {}
for uid, u in list(self.wesnoth.unit_lookup.items()):
if u.hidden:
continue
if grouper.unitfilter(u):
forest.add_node(helpers.UnitNode(u))
units_added[uid] = u
#print(" %d/%d units" % (len(units_added), len(self.wesnoth.unit_lookup)))
# Always add any child units, even if they have been filtered out..
while units_added:
new_units_added = {}
for uid, u in list(units_added.items()):
for auid in u.advance:
if not auid in forest.lookup:
try:
au = self.wesnoth.unit_lookup[auid]
except KeyError:
error_message(
"Warning: Unit %s not found as advancement of %s\n" %
(auid, repr(uid)))
continue
forest.add_node(helpers.UnitNode(au))
new_units_added[auid] = au
units_added = new_units_added
if add_parents:
# Also add parent units
added = True
while added:
added = False
for uid, u in list(self.wesnoth.unit_lookup.items()):
if uid in forest.lookup:
continue
for auid in u.advance:
if auid in forest.lookup:
forest.add_node(helpers.UnitNode(u))
added = True
break
forest.update()
# Partition trees by race/faction of first unit.
groups = {}
breadth = 0
for tree in list(forest.trees.values()):
u = tree.unit
ugroups = grouper.groups(u)
for group in ugroups:
groups[group] = groups.get(group, []) + [tree]
breadth += tree.breadth
thelist = list(groups.keys())
thelist.sort(key=lambda x: grouper.group_name(x))
rows_count = breadth + len(thelist)
# Create empty grid.
rows = []
for j in range(rows_count):
column = []
for i in range(6):
column.append((1, 1, None))
rows.append(column)
# Sort advancement trees by name of first unit and place into the grid.
def by_name(t):
x = T(t.unit, "name")
return "" if x is None else x
def grid_place(nodes, x):
nodes.sort(key=by_name)
for node in nodes:
level = max(0, min(5, node.unit.level))
rows[x][level] = (1, node.breadth, node)
for i in range(1, node.breadth):
rows[x + i][level] = (0, 0, node)
grid_place(node.children, x)
x += node.breadth
return x
x = 0
for group in thelist:
node = helpers.GroupNode(group)
node.name = grouper.group_name(group)
rows[x][0] = (6, 1, node)
for i in range(1, 6):
rows[x][i] = (0, 0, None)
nodes = groups[group]
x += 1
x = grid_place(nodes, x)
self.unitgrid = rows
return len(forest.lookup)
def write_navbar(self, report_type):
def write(line):
self.output.write(line)
def _(msgid, textdomain="wesnoth"):
return self.translate(msgid, textdomain)
all_written_html_files.append((self.isocode, self.output.filename))
languages = self.wesnoth.languages_found
langlist = list(languages.keys())
langlist.sort()
write('<div class="navbar">')
write('<ul class="navbar" role="menu">')
def abbrev(name):
abbrev = name[0]
word_separators = [" ", "_", "+", "(", ")"]
for i in range(1, len(name)):
if name[i] in ["+", "(", ")"] or \
name[i - 1] in word_separators and \
name[i] not in word_separators:
abbrev += name[i]
return abbrev
def add_menu(menuid, name, classes='', url='', is_table_container=False, container_classes=''):
"""
Writes the initial portion of a sidebar item, including the item's
label, the start tag for the popup menu container, and the label
for said menu.
If is_table_container=True, the popup menu prolog is suitable for
a table (currently used for the Language menu). This will be
removed one day, hopefully. (TODO)
The url parameter allows setting a destination URL for the menu
header, which will be a link if it is set to a non-empty string.
"""
html_name = cleantext(name)
html_classes = " ".join((cleantext(classes), "popuptrigger"))
write('<li class="popupcontainer" role="menuitem" aria-haspopup="true">')
write('<a class="' + html_classes + '" href="#">' + html_name + '</a>')
if menuid:
write('<div id="%s" class="popupmenu" role="menu" aria-label="%s">' %
(menuid, html_name))
else:
write('<div class="popupmenu" role="menu" aria-label="%s">' % html_name)
if url:
write('<a class="popupheader" href="' + url + '">' + html_name + '</a>')
else:
write('<div class="popupheader">' + html_name + '</div>')
if not is_table_container:
if not container_classes:
write('<ul>')
else:
write('<ul class="%s">' % cleantext(container_classes))
def add_menuitem_placeholder():
"""Writes a horizontal bar serving as a menu placeholder."""
write('<li class="menuplaceholder">' + HTML_ENTITY_HORIZONTAL_BAR + '</li>')
def add_menuitem(url, label, standalone=False, title=''):
"""
Writes a sidebar item.
If standalone=True, the item will be provided without the list
element tags so it can be used in pretty much any context. In
reality, the option is only provided for use with add_menu() with
is_table_container=True and it will be removed at some point in
the hopefully not-so-far future (TODO).
"""
if not standalone:
write('<li>')
extra_attr = (' title="%s"' % cleantext(title)) if title else ''
write('<a href="%s" role="menuitem"%s>%s</a>' %
(cleantext(url), extra_attr, cleantext(label, quote=False)))
if not standalone:
write('</li>')
def end_menu(is_table_container=False):
"""
Writes the closing tags for a menu started with start_menu().
The is_table_container value used here ought to be the same as the
original.
"""
if not is_table_container:
write('</ul>')
write('</div></li>')
# We may not have all the required info yet so defer writing the
# campaigns/eras navigation.
# Campaigns
add_menu("campaigns_menu", _("addon_type^Campaign"))
write(PRE_PLACEHOLDER_CAMPAIGNS.decode('utf-8'))
end_menu()
# Eras
add_menu("eras_menu", _("Era"))
write(PRE_PLACEHOLDER_ERAS.decode('utf-8'))
end_menu()
# Races / Factions
target = self.target
if self.campaign == "units":
target = "mainline.html"
if not self.is_era:
add_menu("races_menu", _("Race", "wesnoth-lib"))
add_menuitem('mainline.html', _("all", "wesnoth-editor"))
r = {}, {}
for u in list(self.wesnoth.unit_lookup.values()):
race = u.race
racename = T(race, "plural_name")
m = 1
if u:
m = 0
rname = race.get_text_val("id") if race else "none"
if not rname:
rname = "none"
if not racename:
racename = rname
r[m][racename] = rname
racenames = sorted(r[0].items())
if list(r[1].items()):
racenames += [("-", "-")] + sorted(r[1].items())
for racename, rid in racenames:
# Some add-ons use race names consisting of only whitespace for
# hiding races in the UI. We need to skip those since otherwise
# they result in unusual markup (e.g. invisible <a> elements).
if not racename.strip():
continue
if racename == "-":
add_menuitem_placeholder()
else:
url = cleanurl(target) + '#' + cleanurl(racename)
add_menuitem(url, racename)
end_menu()
else:
add_menu("races_menu", _("Factions"))
for row in self.unitgrid:
for column in range(6):
hspan, vspan, un = row[column]
if not un:
continue
if isinstance(un, helpers.GroupNode):
url = cleanurl('../%s/%s.html' % (self.isocode, self.campaign))
url += '#' + cleanurl(un.name)
add_menuitem(url, un.name)
end_menu()
# Add entries for the races also to the navbar itself.
if not self.is_era:
races = {}
for uid, u in list(self.wesnoth.unit_lookup.items()):
if self.campaign != "units" and self.campaign not in u.campaigns:
continue
if u.race:
racename = T(u.race, "plural_name")
else:
racename = "none"
if not racename in races:
races[racename] = []
races[racename].append(uid)
racelist = sorted(races.keys())
for r in racelist:
visible_num = len([uid for uid in races[r] if not self.wesnoth.unit_lookup[uid].hidden])
# Some add-ons use race names consisting of only whitespace for
# hiding races in the UI. We need to skip those since otherwise
# they result in unusual markup (e.g. invisible <a> elements).
if not r.strip() or not visible_num:
continue
race_url = "%s#%s" % (cleanurl(target), cleanurl(r))
use_columns = "nocolumns" if visible_num < 12 else ""
add_menu("", r, "unitmenu", url=race_url, container_classes=use_columns)
for uid in sorted(races[r]):
un = self.wesnoth.unit_lookup[uid]
if un.hidden:
continue
if "mainline" in un.campaigns:
addon = "mainline"
else:
addon = self.addon
link = cleanurl("../../%s/%s/%s.html" % (addon, self.isocode, uid))
name = self.wesnoth.get_unit_value(un, "name",
translation=self.translation.translate)
if not name:
error_message("Warning: Unit uid=%s has no name.\n" % uid)
name = uid
add_menuitem(link, name)
end_menu()
# Languages
add_menu("languages_menu", _("Language", "wesnoth-lib"), is_table_container=True)
cell = 0
col = 0
colcount = 5
write('<table>')
write('<tr>')
for lang in langlist:
cell += 1
col += 1
write('<td>')
filename = self.target if self.addon == 'mainline' else 'mainline.html'
url = cleanurl('../%s/%s' % (lang, filename))
# TODO: Maybe use the language name instead of its code for the label?
add_menuitem(url, lang, title=languages[lang], standalone=True)
write('</td>')
if col >= colcount:
col = 0
if cell < len(langlist):
write('</tr><tr>')
if col:
for i in range(col + 1, colcount + 1):
write('<td></td>')
write('</tr>')
write('</table>')
end_menu(is_table_container=True)
write('<li class="overviewlink"><a href="../../overview.html">Build Report</a></li>')
write('</ul></div>\n')
write('<script>\nwmlunits_menu_setup();\n</script>');
def pic(self, u, x, recursion=0):
if recursion >= 4:
error_message(
"Warning: Cannot find image for unit %s(%s).\n" % (
u.get_text_val("id"), x.name.decode("utf8")))
return None, None
image = self.wesnoth.get_unit_value(x, "image")
portrait = self.wesnoth.get_unit_value(x, "profile")
if not portrait:
bu = self.wesnoth.get_base_unit(u)
if bu:
portrait = self.wesnoth.get_unit_value(bu, "profile")
if not image:
if x.name == b"female":
baseunit = self.wesnoth.get_base_unit(u)
if baseunit:
female = baseunit.get_all(tag="female")
if female:
return self.pic(u, female[0], recursion=recursion + 1)
else:
# no female images found, fall back on the male/default ones
error_message("Warning: missing female image for unit \
{}, falling back to male/default image.\n".format(u.get_text_val("id")))
return self.pic(u, u, recursion=recursion + 1)
else:
return self.pic(u, u, recursion=recursion + 1)
error_message("Warning: Missing image for unit %s(%s).\n" %
(u.get_text_val("id"), x.name.decode("utf8")))
return None, None
icpic = image_collector.add_image_check(self.addon, os.path.normpath(image))
if not icpic.ipath:
error_message("Warning: No picture %s for unit %s.\n" %
(image, u.get_text_val("id")))
picname = icpic.id_name
image = os.path.join(PICS_LOCATION, picname)
if portrait:
picname = image_collector.add_image(self.addon,
portrait,
no_tc=True,
check_transparent=True)
portrait = os.path.join(PICS_LOCATION, picname)
return path2url(image), path2url(portrait)
def get_abilities(self, u):
anames = []
already = {}
for abilities in u.get_all(tag="abilities"):
try:
c = abilities.get_all()
except AttributeError:
c = []
for ability in c:
try:
id = ability.get_text_val("id")
except AttributeError as e:
error_message("Error: Ignoring ability %s" % ability.debug())
continue
if id in already:
continue
already[id] = True
name = T(ability, "name")
if not name:
name = ability.name.decode("utf8")
# Only add abilities with a label, since those that lack one
# are normally hidden in the game and used to implement more
# complex ones.
if name:
anames.append(name)
return anames
def get_recursive_attacks(self, this_unit):
def copy_attributes(copy_from, copy_to):
for c in copy_from.data:
if isinstance(c, wmlparser3.AttributeNode):
copy_to.data.append(c)
# Use attacks of base_units as base, if we have one.
base_unit = self.wesnoth.get_base_unit(this_unit)
attacks = []
if base_unit:
attacks = copy.deepcopy(self.get_recursive_attacks(base_unit))
base_attacks_count = len(attacks)
for i, attack in enumerate(this_unit.get_all(tag="attack")):
# Attack merging is order based.
if i < base_attacks_count:
copy_attributes(attack, attacks[i])
else:
attacks.append(attack)
return attacks
def write_units(self):
def write(line):
self.output.write(line)
def _(msgid, textdomain="wesnoth"):
return self.translate(msgid, textdomain)
rows = self.unitgrid
write('<table class="units">\n<colgroup>')
for i in range(6):
write('<col class="col%d" />' % i)
write('</colgroup>')
pic = image_collector.add_image(
"general",
os.path.join("..", "..", "..", "images", "misc", "leader-crown.png"),
no_tc=True
)
crownimage = cleanurl(path2url(os.path.join(PICS_LOCATION, pic)))
ms = None
for row in range(len(rows)):
write('<tr>\n')
for column in range(6):
hspan, vspan, un = rows[row][column]
if vspan:
attributes = ""
if hspan == 1 and vspan == 1:
pass
elif hspan == 1:
attributes += ' rowspan="%d"' % vspan
elif vspan == 1:
attributes += ' colspan="%d"' % hspan
if un and isinstance(un, helpers.GroupNode):
# Find the current multiplayer side so we can show the
# little crowns..
ms = None
if self.is_era:
try:
eid, fid = un.data
era = self.wesnoth.era_lookup[eid]
if fid:
ms = era.faction_lookup[fid]
except TypeError:
pass
racename = un.name
# TODO: we need to use a unique race id instead of a potentially duplicate
# name for the header id and link target!
attributes += ' id="%s" class="raceheader"' % cleantext(racename)
write('<th' + attributes + '>')
write('<a href="#%s">%s</a>' % (cleanurl(racename), cleantext(racename, quote=False)))
write('</th>\n')
elif un:
u = un.unit
attributes += ' class="unitcell"'
write('<td%s>' % attributes)
def uval(name):
return self.wesnoth.get_unit_value(u, name,
translation=self.translation.translate)
def clean_uval(name):
return cleantext(uval(name))
uid = cleantext(u.get_text_val("id"))
name = clean_uval("name")
cost = clean_uval("cost")
hp = clean_uval("hitpoints")
mp = clean_uval("movement")
xp = clean_uval("experience")
level = clean_uval("level")
crown = ""
if ms:
if un.id in ms.units:
crown = ""
if un.id in ms.is_leader:
crown = ""
uaddon = "mainline"
if "mainline" not in u.campaigns:
uaddon = self.addon
link = cleanurl("../../%s/%s/%s.html" % (uaddon, self.isocode, uid))
write('<div class="l">L%s%s</div>' % (level, crown))
write('<a href="%s" title="Id: %s">%s</a><br />' % (link, uid, name))
write('<div class="pic">')
image, portrait = self.pic(u, u)
image = cleanurl(image)
write('<a href="%s" title="Id: %s">' % (link, uid))
if crown == "":
write('<div class="spritebg" style="background-image:url(\'%s\')">' % image)
write('<img src="%s" alt="(image)" />' % crownimage)
write('</div>')
else:
write('<img src="%s" alt="(image)" />' % image)
write('</a>\n</div>\n')
write('<div class="attributes">')
write('<table><colgroup><col class="attribute-label"><col class="attribute-value">')
attributes = (
(_("Cost: "), cost),
(_("HP: "), hp),
(_("XP: "), xp),
(_("MP: "), mp),
)
for attr_label, attr_value in attributes:
write('<tr><th>%s</th><td>%s</td></tr>' % (cleantext(attr_label.strip(), quote=False), attr_value))
write('</table>')
# Write info about abilities.
anames = self.get_abilities(u)
if anames:
write('\n<div class="abilities">')
write(cleantext(", ".join(anames)))
write('</div>')
# Write info about attacks.
attacks = self.get_recursive_attacks(u)
if attacks:
write('\n<div class="attacks">')
first_attack = True
for attack in attacks:
if not first_attack:
write('<br />')
first_attack = False
r = T(attack, "range")
t = T(attack, "type")
range_icon = image_collector.add_image_check(self.addon, os.path.normpath('icons/profiles/%s_attack.png' % r), no_tc=True)
range_icon = cleanurl(path2url(os.path.join(PICS_LOCATION, range_icon.id_name)))
range_alt_text = 'attack range %s' % cleantext(_(r), quote=False)
type_icon = image_collector.add_image_check(self.addon, os.path.normpath('icons/profiles/%s.png' % t), no_tc=True)
type_icon = cleanurl(path2url(os.path.join(PICS_LOCATION, type_icon.id_name)))
type_alt_text = 'attack type %s' % cleantext(_(t), quote=False)
x = '<img src="%s" alt="(%s)"/> <img src="%s" alt="(%s)"/> ' % (range_icon, range_alt_text, type_icon, type_alt_text)
write (x)
n = T(attack, "number")
x = T(attack, "damage")
x = "%s %s %s " % (cleantext(x, quote=False), HTML_ENTITY_MULTIPLICATION_SIGN, cleantext(n, quote=False))
write(x)
x = '<br/>%s-%s' % (_(r), _(t))
write(x)
s = []
specials = attack.get_all(tag="specials")
if specials:
for special in specials[0].get_all(tag=""):
sname = T(special, "name")
if sname:
s.append(sname)
accuracy = attack.get_text_val("accuracy", default="0")
parry = attack.get_text_val("parry", default="0")
if accuracy != "0":
s.append("accuracy "+accuracy+"%")
if parry != "0":
s.append("parry "+parry+"%")
if s:
s = ", ".join(s)
write(" (%s)" % cleantext(s, quote=False))
write('</div>')
write('</div>')
write('</td>\n')
else:
write('<td class="empty"></td>')
write('</tr>\n')
write('</table>\n')
def write_units_tree(self, grouper, title, add_parents):
html_title = cleantext(title)
self.output.write(website_header(title=html_title,
classes=['wmlunits-tree']))
n = self.analyze_units(grouper, add_parents)
self.write_navbar("units_tree")
self.output.write('<div class="main">')
self.output.write('<h1>%s</h1>' % html_title)
self.write_units()
self.output.write(HTML_CLEAR_FLOATS)
self.output.write(build_timestamp())
self.output.write('</div>')
self.output.write(website_footer())
return n
def write_unit_report(self, output, unit):
def write(line):
self.output.write(line)
def _(msgid, textdomain="wesnoth"):
return self.translate(msgid, textdomain)
def find_attr(what, key):
if unit.movetype:
mtx = unit.movetype.get_all(tag=what)
mty = None
if mtx:
mty = mtx[0].get_text_val(key)
x = unit.get_all(tag=what)
y = None
if x:
y = x[0].get_text_val(key, translation=self.translation.translate)
if y:
return True, y
if unit.movetype and mty is not None:
return False, mty
return False, "-"
def uval(name):
return self.wesnoth.get_unit_value(unit, name, translation=self.translation.translate)
def clean_uval(name):
return cleantext(uval(name))
# Write unit name, picture and description.
uid = unit.get_text_val("id")
uname = uval("name")
display_name = cleantext(uname)
self.output = output
write(website_header(title=display_name,
classes=['wmlunits-unit']))
self.write_navbar("unit_report")
self.output.write('<div class="main">')
female = unit.get_all(tag="female")
if female:
fname = T(female[0], "name")
if fname and fname != uname:
display_name += " / " + cleantext(fname)
write('<div id="unit-summary" class="unit-columns">\n')
write('<div id="unit-desc" class="unit-column-left">\n')
write('<h1>%s</h1>\n' % display_name)
write('<div id="unit-sprites" class="pic">')
if female:
mimage, portrait = self.pic(unit, unit)
fimage, fportrait = self.pic(unit, female[0])
if not fimage:
fimage = mimage
if not fportrait:
fportrait = portrait
write('<img src="%s" alt="(image)" />\n' % cleanurl(mimage))
write('<img src="%s" alt="(image)" />\n' % cleanurl(fimage))
image = mimage
else:
image, portrait = self.pic(unit, unit)
write('<img src="%s" alt="(image)" />\n' % cleanurl(image))
write('</div>\n')
description = clean_uval("description")
# TODO: what is unit_description?
if not description:
description = clean_uval("unit_description")
if not description:
description = HTML_ENTITY_HORIZONTAL_BAR
write('<p>%s</p>\n' % re.sub('\n', '\n<br />', description))
write('</div> <!-- #unit-desc -->\n')
write('<div id="unit-portraits" class="unit-column-right">\n')
for si in range(2):
if si and not female:
break
if si:
sportrait = fportrait
simage = fimage
else:
simage = image
sportrait = portrait
write('<div class="portrait">')
if portrait:
write('<img src="%s" alt="(portrait)" />\n' % cleanurl(sportrait))
else:
write('<div style="background-image:url(\'%s\')">&nbsp;</div>' % cleanurl(simage))
write('</div>')
write('</div>\n')
write('</div> <!-- #unit-summary -->\n')
write('<div id="unit-details" class="unit-columns">\n<div class="unit-column-left">\n')
write('<h2>Information</h2>\n')
write('<table class="unitinfo">\n')
# Advances-from list
write('<tr><th>%s</th><td>' % cleantext(_("Advances from: "), quote=False))
have_advances = False
for pid in self.forest.get_parents(uid):
punit = self.wesnoth.unit_lookup[pid]
if "mainline" in unit.campaigns and "mainline" not in punit.campaigns:
continue
addon = "mainline" if "mainline" in unit.campaigns else self.addon
link = cleanurl("../../%s/%s/%s.html" % (addon, self.isocode, pid))
name = self.wesnoth.get_unit_value(punit, "name",
translation=self.translation.translate)
if have_advances:
write(', ')
write('<a href="%s">%s</a>' % (link, cleantext(name, quote=False)))
have_advances = True
if not have_advances:
write(HTML_ENTITY_FIGURE_DASH)
write('</td></tr>\n')
# Advances-to list
write('<tr><th>%s</th><td>' % cleantext(_("Advances to: "), quote=False))
have_advances = False
for cid in self.forest.get_children(uid):
try:
cunit = self.wesnoth.unit_lookup[cid]
addon = "mainline" if "mainline" in cunit.campaigns else self.addon
link = cleanurl("../../%s/%s/%s.html" % (addon, self.isocode, cid))
if "mainline" in unit.campaigns and "mainline" not in cunit.campaigns:
continue
name = self.wesnoth.get_unit_value(cunit, "name",
translation=self.translation.translate)
except KeyError:
error_message("Warning: Unit %s not found.\n" % cid)
name = cid
if "mainline" in unit.campaigns:
continue
link = cleanurl(self.target)
if have_advances:
write(', ')
write('<a href="%s">%s</a>' % (link, cleantext(name, quote=False)))
have_advances = True
if not have_advances:
write(HTML_ENTITY_FIGURE_DASH)
write('</td></tr>\n')
attributes = [
("cost", _("Cost: ")),
("hitpoints", _("HP: ")),
("movement", _("Moves: ")),
("vision", _("Vision: ")),
("jamming", _("Jamming: ")),
("experience", _("XP: ")),
("level", _("Level: ")),
("alignment", _("Alignment: ")),
("id", "Id: ")
]
for attr, label in attributes:
value = uval(attr)
if not value and attr in ("jamming", "vision"):
continue
if attr == "alignment":
value = _(value)
write('<tr><th>%s</th><td class="val">%s</td></tr>\n' % (cleantext(label, quote=False), cleantext(value, quote=False)))
# Write info about abilities.
anames = self.get_abilities(unit)
write('<tr>\n')
write('<th>%s</th>' % cleantext(_("Abilities: "), quote=False))
if len(anames):
write('<td class="val">' + cleantext(', '.join(anames), quote=False) + '</td>')
else:
write('<td>' + HTML_ENTITY_FIGURE_DASH + '</td>')
write('</tr>\n')
write('</table>\n')
# Write info about attacks.
attacks = self.get_recursive_attacks(unit)
if attacks:
write('<h2>%s <small>(damage %s count)</small></h2>\n' %
(cleantext(_("unit help^Attacks"), quote=False),
HTML_ENTITY_MULTIPLICATION_SIGN))
write('<table class="unitinfo attacks">\n')
write('<colgroup><col class="col0" /><col class="col1" /><col class="col2" /><col class="col3" /></colgroup>')
for attack in attacks:
write('<tr>')
aid = attack.get_text_val("name")
aname = T(attack, "description")
icon = attack.get_text_val("icon")
if not icon:
icon = "attacks/%s.png" % aid
image_add = image_collector.add_image_check(self.addon, os.path.normpath(icon), no_tc=True)
if not image_add.ipath:
error_message("Error: No attack icon '%s' found for '%s'.\n" % (
icon, uid))
# core/images/units/elves-wood/shaman.png
icon = os.path.join(PICS_LOCATION, "shaman..Y29yZS9pbWFnZXMvdW5pdHMvZWx2ZXMtd29vZA.png")
else:
icon = os.path.join(PICS_LOCATION, image_add.id_name)
write('<td><img src="%s" alt="(image)"/></td>' % cleanurl(icon))
write('<td><b>%s</b></td>' % cleantext(aname, quote=False))
t = T(attack, "type")
type_icon = image_collector.add_image_check(self.addon, os.path.normpath('icons/profiles/%s.png' % t), no_tc=True)
type_icon = cleanurl(os.path.join(PICS_LOCATION, type_icon.id_name))
type_alt_text = cleantext('%s attack' % t, quote=False)
x = '<td><img src="%s" alt="(%s)"/> %s</td>' % (type_icon, type_alt_text, cleantext(_(t), quote=False))
write(x)
n = attack.get_text_val("number")
x = attack.get_text_val("damage")
x = '%s %s %s' % (cleantext(x, quote=False), HTML_ENTITY_MULTIPLICATION_SIGN, cleantext(n, quote=False))
write('<td><i>%s</i></td>' % x)
r = T(attack, "range")
range_icon = image_collector.add_image_check(self.addon, os.path.normpath('icons/profiles/%s_attack.png' % r), no_tc=True)
range_icon = cleanurl(os.path.join(PICS_LOCATION, range_icon.id_name))
range_alt_text = cleantext('%s attack' % r, quote=False)
x = '<td><img src="%s" alt="(%s)"/> %s</td>' % (range_icon, range_alt_text, cleantext(_(r), quote=False))
write(x)
s = []
specials = attack.get_all(tag="specials")
if specials:
for special in specials[0].get_all(tag=""):
sname = T(special, "name")
if sname:
s.append(cleantext(sname, quote=False))
accuracy = attack.get_text_val("accuracy", default="0")
parry = attack.get_text_val("parry", default="0")
if accuracy != "0":
s.append(cleantext("accuracy "+accuracy+"%"))
if parry != "0":
s.append(cleantext("parry "+parry+"%"))
if s:
write('<td>(%s)</td>' % ', '.join(s))
write('</tr>')
write('</table>\n')
# Write info about resistances.
write('<h2>%s</h2>\n' % _("Resistances: ").strip(" :"))
write('<table class="unitinfo resistances">\n')
write('<colgroup><col class="col0" /><col class="col1" /><col class="col2" /><col class="col3" /><col class="col4" /><col class="col5" /><col class="col6" /></colgroup>')
row = 0
for rid, ricon in RESISTANCES:
special, resist_str = find_attr("resistance", rid)
r = 100 if resist_str == '-' else 100 - int(resist_str)
resist_classes = ['num']
resist_rating = resistance_rating_color_class(r)
if resist_rating:
resist_classes.append('rating-' + resist_rating)
try:
resist_str = '<i>%d%%</i>' % r
except ValueError:
error_message("Warning: Invalid resistance %s for %s.\n" % (
r, uid))
rcell = "td"
if special:
rcell += ' class="special"'
if row % 2 == 0:
write('<tr>\n')
else:
write('<td></td>')
picname = image_collector.add_image(self.addon, os.path.normpath(ricon), no_tc=True)
icon = os.path.join(PICS_LOCATION, picname)
write('<td><img src="%s" alt="(icon)" /></td>\n' % (icon, ))
write('<th>%s</th><td class="%s">%s</td>\n' % (cleantext(_(rid), quote=False), ' '.join(resist_classes), resist_str))
if row % 2 == 1:
write('</tr>\n')
row += 1
write('</table>\n')
# end left column
write('</div>')
write('<div class="unit-column-right">')
# Write info about movement costs and terrain defense.
write('<h2>' + cleantext(_("Terrain"), quote=False) + '</h2>\n')
write('<table class="unitinfo terrain">\n')
write('<colgroup><col class="col0" /><col class="col1" /><col class="col2" /><col class="col3" /></colgroup>')
write('<thead>')
write('<tr><th colspan="2"><span class="sr-label">%s</span></th><th class="mvtcost">%s</th><th class="numheader">%s</th></tr>\n' % (
cleantext(_("Terrain"), quote=False),
cleantext(_("Movement Cost"), quote=False),
cleantext(_("Defense"), quote=False)))
write('</thead>')
terrains = self.wesnoth.terrain_lookup
terrainlist = []
already = {}
for tstring, t in list(terrains.items()):
tid = t.get_text_val("id")
if tid in HIDDEN_TERRAINS or t.get_all(att="aliasof") or tid in already:
continue
already[tid] = 1
name = T(t, "name")
ticon = t.get_text_val("symbol_image")
if not ticon:
ticon = t.get_text_val("icon_image")
# Use nice images for known mainline terrain types
if tid in TERRAIN_ICONS:
ticon = TERRAIN_ICONS[tid]
if ticon:
terrainlist.append((name, tid, ticon))
else:
error_message("Terrain %s has no symbol_image\n" % tid)
terrainlist.sort()
for tname, tid, ticon in terrainlist:
not_from_race, move_cost = find_attr("movement_costs", tid)
classes_cost = ['mvtcost']
cost_rating = ''
not_from_race, defense = find_attr("defense", tid)
classes_defense = ['num']
defense_rating = ''
if defense == '-':
defense = 100
total_movement = uval('movement')
cost_rating = mvtcost_rating_color_class(move_cost, total_movement)
try:
defense = int(defense)
# negative defense has something to do with best defense if
# there's multiple terrain types
if defense < 0:
defense = -defense
defense_rating = defense_rating_color_class(100 - defense)
defense = "%d%%" % (100 - defense)
except ValueError:
error_message("Warning: Invalid defense %s for %s.\n" % (
defense, uid))
if cost_rating:
classes_cost.append('rating-' + cost_rating)
if defense_rating:
classes_defense.append('rating-' + defense_rating)
if move_cost == '-' or int_fallback(total_movement) < int_fallback(move_cost, 99):
move_cost = HTML_ENTITY_FIGURE_DASH
else:
move_cost = cleantext(move_cost, quote=False)
write('<tr>\n')
picname = image_collector.add_image(self.addon,
os.path.normpath("terrain/%s.png" % ticon),
no_tc=True)
icon = os.path.join(PICS_LOCATION, picname)
write('<td><img src="%s" alt="(icon)" /></td>\n' % cleanurl(icon))
write('<td>%s</td><td class="%s"><i>%s</i></td><td class="%s"><i>%s</i></td>\n' %
(cleantext(tname, quote=False),
' '.join(classes_cost), move_cost,
' '.join(classes_defense), defense))
write('</tr>\n')
write('</table>\n')
write('</div>\n')
write('</div> <!-- #unit-details -->\n')
self.output.write(HTML_CLEAR_FLOATS)
self.output.write(build_timestamp())
write('</div>') # main
self.output.write(website_footer())
def generate_campaign_report(addon, isocode, campaign, wesnoth):
if campaign:
cid = campaign.get_text_val("id")
else:
cid = "mainline"
if not cid:
cid = "%s_%s" % (addon, campaign.get_text_val("define"))
print("campaign %s %s %s" % (addon, cid, isocode))
path = os.path.join(options.output, addon, isocode)
if not os.path.isdir(path):
os.mkdir(path)
output = MyFile(os.path.join(path, "%s.html" % cid), "w")
html = HTMLOutput(isocode, output, addon, cid, False, wesnoth)
html.target = "%s.html" % cid
grouper = GroupByRace(wesnoth, cid)
if campaign:
title = campaign.get_text_val("name", translation=html.translate)
else:
title = html.translate("Units", "wesnoth-help")
if not title:
title = cid
n = html.write_units_tree(grouper, title, True)
output.close()
return n
def generate_era_report(addon, isocode, era, wesnoth):
eid = era.get_text_val("id")
print("era %s %s %s" % (addon, eid, isocode))
path = os.path.join(options.output, addon, isocode)
if not os.path.isdir(path):
os.mkdir(path)
output = MyFile(os.path.join(path, "%s.html" % eid), "w")
html = HTMLOutput(isocode, output, addon, eid, True, wesnoth)
html.target = "%s.html" % eid
grouper = GroupByFaction(wesnoth, eid)
ename = era.get_text_val("name", translation=html.translate)
n = html.write_units_tree(grouper, ename, False)
output.close()
return n
def generate_single_unit_reports(addon, isocode, wesnoth):
path = os.path.join(options.output, addon, isocode)
if not os.path.isdir(path):
os.mkdir(path)
html = HTMLOutput(isocode, None, addon, "units", False, wesnoth)
grouper = GroupByNothing()
html.analyze_units(grouper, True)
for uid, unit in list(wesnoth.unit_lookup.items()):
if unit.hidden:
continue
if "mainline" in unit.campaigns and addon != "mainline":
continue
try:
htmlname = "%s.html" % uid
filename = os.path.join(path, htmlname)
# We probably can come up with something better.
if os.path.exists(filename):
age = time.time() - os.path.getmtime(filename)
# was modified in the last 12 hours - we should be ok
if age < 3600 * 12:
continue
except (UnicodeDecodeError, UnicodeEncodeError) as e:
traceback.print_exc()
error_message("Unicode problem: " + repr(path) + " + " + repr(uid) + "\n")
error_message(str(e) + "\n")
continue
output = MyFile(filename, "w")
html.target = "%s.html" % uid
html.write_unit_report(output, unit)
output.close()
popup_campaigns_html = {}
popup_eras_html = {}
def get_popup_campaigns_html(isocode, batchlist):
if isocode in popup_campaigns_html:
return popup_campaigns_html[isocode]
chtml = ""
cids = [[], []]
for addon in batchlist:
for campaign in addon.get("campaigns", []):
if campaign["units"] == "?" or campaign["units"] <= 0:
continue
lang = isocode if addon["name"] == "mainline" else "en_US"
c = addon["name"], campaign["id"], campaign["translations"].get(
lang, campaign["name"]), lang
if addon["name"] == "mainline":
cids[0].append(c)
else:
cids[1].append(c)
for i in range(2):
campaigns = cids[i]
campaigns.sort(key=lambda x: "A" if x[1] == "mainline" else "B" + x[2])
for campaign in campaigns:
addon, cname, campname, lang = campaign
url = cleanurl("../../%s/%s/%s.html" % (addon, lang, cname))
chtml += '<li><a title="%s" href="%s" role="menuitem">%s</a></li>\n' % (
cleantext(campname), url, cleantext(campname, quote=False))
if i == 0 and cids[1]:
chtml += '</ul><ul>'
popup_campaigns_html[isocode] = bytes(chtml, "utf-8")
return popup_campaigns_html[isocode]
def get_popup_eras_html(isocode, batchlist):
if isocode in popup_eras_html:
return popup_eras_html[isocode]
ehtml = ""
eids = [[], []]
for addon in batchlist:
for era in addon.get("eras", []):
if era["units"] == "?" or era["units"] <= 0:
continue
lang = isocode if addon["name"] == "mainline" else "en_US"
e = addon["name"], era["id"], era["translations"].get(
lang, era["name"]), lang
if addon["name"] == "mainline":
eids[0].append(e)
else:
eids[1].append(e)
for i in range(2):
eras = eids[i]
eras.sort(key=lambda x: x[2])
for era in eras:
addon, eid, eraname, lang = era
url = cleanurl("../../%s/%s/%s.html" % (addon, lang, eid))
ehtml += '<li><a title="%s" href="%s" role="menuitem">%s</a></li>\n' % (
cleantext(eraname), url, cleantext(eraname, quote=False))
if i == 0 and eids[1]:
ehtml += '</ul><ul>'
popup_eras_html[isocode] = bytes(ehtml, "utf-8")
return popup_eras_html[isocode]
def html_postprocess_file(filename, isocode, batchlist):
f = open(filename, "r+b")
b_html = f.read()
b_html = b_html.replace(PRE_PLACEHOLDER_CAMPAIGNS, get_popup_campaigns_html(isocode, batchlist))
b_html = b_html.replace(PRE_PLACEHOLDER_ERAS, get_popup_eras_html(isocode, batchlist))
f.seek(0)
f.write(b_html)
f.close()
def html_postprocess_all(batchlist):
print("Postprocessing HTML...")
for isocode, filename in all_written_html_files:
html_postprocess_file(filename, isocode, batchlist)
popup_eras_html.clear()
popup_campaigns_html.clear()
def write_index(out_path):
output = MyFile(os.path.join(out_path, "index.html"), "w")
output.write("""
<html><head>
<meta http-equiv="refresh" content="0;url=mainline/en_US/mainline.html">
</head>
<body>
<a href="mainline/en_US/mainline.html">Redirecting to Wesnoth units database...</a>
</body>
</html>
""")