Legend of the Invincibles

Discussion and development of scenarios and campaigns for the game.

Moderators: Forum Moderators, Developers

Post Reply

Which of these units you find worth advancing and gearing heavily? Unpopular ones will be reworked.

Prophet
10
16%
Reaper
5
8%
Scythemaster
4
6%
Shadowalker
3
5%
Shadow Prince
7
11%
Siege Troll
3
5%
Sky Goblin
1
2%
Snow Hunter
7
11%
Soul Shooter
2
3%
Swordmaster
12
19%
Troll Boulderlobber
0
No votes
Warlock
5
8%
Werewolf Rider
2
3%
Zombie Rider
1
2%
 
Total votes: 62

Pags
Posts: 39
Joined: January 19th, 2016, 5:56 pm

Re: Legend of the Invincibles

Post by Pags »

Zarkussell wrote:@dugi
There seems to be a bug in chapter 5 after you complete several scenarios with the configure recall list button where the game simply freezes until you close it and reopen it.
This was happening to me as well. I think it became an issue once one of the characters on the autorecall list had died (if I remember correctly, in my case it was an unnamed wrath). In other words, I could alter my recall list until someone died. Once they died, going into the autorecall dialogues crashed the game.
Pags
Posts: 39
Joined: January 19th, 2016, 5:56 pm

Re: Legend of the Invincibles

Post by Pags »

matsjoyce wrote:OK, found some problems. I don't know how it sorted for me, but I've given it a good testing now (~70 items added), and it works great. I've attached a diff (right way round now :oops: )). If the inventory already contains items, it will probably do some sort of grouping, but won't sort the existing items, so its a sort of clean start requiring feature.
I'm not proficient with either lua or wml, but wouldn't another fresh-start solution to be to give everything an item number (or hash table plus an associated pre-sorted index) and starting everything with item quantity = 0. Then when making the menu selection list, only list those with positive quantity?

An issue I see with this proposal is that if additional items are added to the game code as development occurs, all of a sudden your items might morph if they appear in a slightly different spot in the list (i.e. if what was once item 2 is now item 3). I don't know if mapped key/values would be a way to work around this or if wml mapping/filtering functions are too slow.
User avatar
Dugi
Posts: 4960
Joined: July 22nd, 2010, 10:29 am
Location: Carpathian Mountains
Contact:

Re: Legend of the Invincibles

Post by Dugi »

@Pags
Do you have a save file where the autorecall issue can be replicated?

Some items are not just numbers. The crafted items have an additional attribute, the base type. That would not work with purely marking counts of each item. I was also considering an option to allow gems to slightly alter the properties of items, so I designed it to allow this possibility.
Pags
Posts: 39
Joined: January 19th, 2016, 5:56 pm

Re: Legend of the Invincibles

Post by Pags »

Dugi wrote:@Pags
Do you have a save file where the autorecall issue can be replicated?
I'll have to check. I assume you'd want before-and-after save games (before = recall list works fine, after=crash whenever you access the menu). As it's a crash out of the game client (AFAIR) it could be platform dependent. In which case, all will be Ubuntu Linux (version 14?)
Some items are not just numbers. The crafted items have an additional attribute, the base type. That would not work with purely marking counts of each item. I was also considering an option to allow gems to slightly alter the properties of items, so I designed it to allow this possibility.
Good points. Speaking of which, at least in the Beautiful Child campaign, crafting a weapon results in an icon of a backpack/pouch, rather than grabbing a generic image of that weapon type. Sword of thorns (a random drop) graphic appears as just a black background when involved in combat. I think it had a sword image when displayed on the map pre-pickup. Do you need savegame files for that too?
User avatar
Dugi
Posts: 4960
Joined: July 22nd, 2010, 10:29 am
Location: Carpathian Mountains
Contact:

Re: Legend of the Invincibles

Post by Dugi »

As it's a crash out of the game client (AFAIR) it could be platform dependent. In which case, all will be Ubuntu Linux (version 14?)
I believe it's not platform dependent. And I also have Ubuntu 14(.10.), so it does not matter anyway.
Speaking of which, at least in the Beautiful Child campaign, crafting a weapon results in an icon of a backpack/pouch, rather than grabbing a generic image of that weapon type.
This applies everywhere. I was lazy to add some special visuals for the case when it's on the ground, because the cases when you actually put it on the ground for any reason are rare.
Sword of thorns (a random drop) graphic appears as just a black background when involved in combat. I think it had a sword image when displayed on the map pre-pickup. Do you need savegame files for that too?
I have fixed this already, just not uploaded yet.
User avatar
Dugi
Posts: 4960
Joined: July 22nd, 2010, 10:29 am
Location: Carpathian Mountains
Contact:

Re: Legend of the Invincibles

Post by Dugi »

The investigation of the save file of a leader not attacking even a completely vulnerable enemy has shown that it was an old save file that did not have the custom AI enabled. Therefore, it would be helpful to me if someone attached a save file where a leader does not attack an unprotected unit (he should not rush into a group of your units)?
User avatar
matsjoyce
Posts: 233
Joined: May 8th, 2011, 2:10 pm
Location: UK

Re: Legend of the Invincibles

Post by matsjoyce »

@Pags
Pags wrote: It would be helpful for you to provide links to help about the weapon special attributes. E.g. link to a page that shows what "focused" means (if that's possible and relatively trivial)
Done! Version 0.3 of the script now generates a page containing abilities and weapon specials with their descriptions at https://wiki.wesnoth.org/LotI_Abilities. Latest code:

Code: Select all

#!/usr/bin/env python3
#
# Copyright 2016 Matthew Joyce
#
# This program is free software: you can redistribute it and/or modify it
# under the terms of the GNU Lesser General Public License as published
# by the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
# GNU Lesser General Public License for more details.
#
# You should have received a copy of the GNU Lesser General Public License
# along with this program.  If not, see <http://www.gnu.org/licenses/>.

import argparse
import pathlib
import re
import collections
import itertools
import builtins
import time
import sys
import shlex

__version__ = "0.3"

wml_regexes = [("key", r"(\w+)\s*=\s*_?\s*\"([^\"]*)\""),
               ("key", r"(\w+)\s*=\s*([^\n]+)"),
               ("keys", r"([\w,]+)\s*=\s*([^\n]+)"),
               ("open", r"\[(\w+)\]"),
               ("close", r"\[/(\w+)\]"),
               ("macro_open", r"(\{[^{}]+)"),
               ("pre", r"#(define|ifdef|else|endif|enddef) ?([^\n]*)"),
               ("whitespace", r"(\s+)"),
               ("comment", r"#[^\n]*")]

wml_regexes = [(n, re.compile(r, re.DOTALL)) for n, r in wml_regexes]

levels = ["EASY", "MEDIUM", "HARD"]

sort_translations = {"weaponword": "craftable as any weapon",
                     "armourword": "craftable as any armour",
                     }

weapon_sorts = ["sword", "axe", "bow", "mace", "xbow", "spear", "dagger", "knife", "staff",
                "weaponword", "polearm", "sling", "thunderstick", "claws", "essence", "exotic"]

damage_ranges = ["", "_melee", "_ranged"]
damage_types = ["blade", "impact", "pierce", "fire", "cold", "arcane"]

defence_types = [("forest", "in forests"),
                 ("frozen", "on frozen places"),
                 ("flat", "on flat terrains"),
                 ("cave", "in caves"),
                 ("fungus", "in mushroom groves"),
                 ("village", "in villages"),
                 ("castle", "in castles"),
                 ("shallow_water", "in shallow waters"),
                 ("reef", "on coastal reefs"),
                 ("deep_water", "in deep water"),
                 ("swamp_water", "in swamps"),
                 ("hills", "on hills"),
                 ("mountains", "on mountains"),
                 ("sand", "on sands"),
                 ("unwalkable", "above unwalkable places"),
                 ("impassable", "inside impassable walls")]

movement_costs = [("forest", "through forests"),
                  ("frozen", "on frozen lands"),
                  ("flat", "on flat terrains"),
                  ("cave", "through dark caves"),
                  ("fungus", "through mushroom groves"),
                  ("village", "through villages"),
                  ("castle", "through castles"),
                  ("shallow_water", "in shallow waters"),
                  ("reef", "on coastal reefs"),
                  ("deep_water", "in deep waters"),
                  ("swamp_water", "through swampy places"),
                  ("hills", "on hills"),
                  ("mountains", "on mountains"),
                  ("sand", "across sands"),
                  ("unwalkable", "above unwalkable places"),
                  ("impassable", "through impassable walls")]

special_translation = {"NOSFERATU_GORGE": "nosferatu's gorge",
                       "RAIJERS_SALOON": "Raijer's saloon"
                       }

ability_translation = {"BERSERK_LEADERSHIP": "radiating insanity",
                       "CHARGE_LEADERSHIP": "warlord's rule",
                       "POISON_LEADERSHIP": "radiation",
                       "FIRSTSTRIKE_LEADERSHIP": "zeal aura",
                       "BACKSTAB_LEADERSHIP": "murderous presence",
                       "MARKSMAN_LEADERSHIP": "cantor",
                       "DRAIN_LEADERSHIP": "aura of hunger",
                       "REGENERATES_LESSER": "regenerates slightly",
                       "PENETRATE_LEADERSHIP": "push",
                       "DARKENS_IMPROVED": "darkens badly",
                       "DARKENS_GREAT": "darkens severely",
                       "ILLUMINATES_IMPROVED": "improved illumination",
                       "ILLUMINATES_GREAT": "great illumination",
                       "LEADERSHIP_LEVEL_6": "leadership",
                       "WEAK_AMBUSH": "lesser ambush",
                       "IMMUNE_TO_SLOW": "resistant to slow",
                       "PENETRATE": "penetrates",
                       "TOXIC_AURA": "dark aura",
                       "FEEDING_EASY": "feeding",
                       }

header = """
This is an auto-generated wiki page listing {{}} currently avalible in the campaign "Legend of the Invincibles". {{}}
This was generated at {} using version {{}} of LotI and version {} of the generation script.
As this is auto-generated, DO NOT EDIT THIS PAGE.
Instead, make suggestions at https://forums.wesnoth.org/viewtopic.php?f=8&t=32384 and the script will be adjusted.

Other LotI-related wiki pages:

* https://wiki.wesnoth.org/LotI_Items &ndash; items, such as weapons and books
* https://wiki.wesnoth.org/LotI_Standard_Advancements &ndash; general advancements such as legacies and books
* https://wiki.wesnoth.org/LotI_Unit_Advancements &ndash; unit specific advancements
* https://wiki.wesnoth.org/LotI_Abilities &ndash; abilities and weapon specials
* https://wiki.wesnoth.org/DeadlyUnitsFromLotI
""".lstrip().format(time.ctime(), __version__)


WMLTag = collections.namedtuple("WMLTag", ("keys", "tags", "annotation", "macros"))


class WMLValue:
    def __init__(self):
        self.EASY = self.MEDIUM = self.HARD = ""

    @property
    def any(self):
        return self.EASY or self.MEDIUM or self.HARD

    @property
    def all(self):
        return self.EASY == self.MEDIUM == self.HARD

    def iter(self):
        yield "EASY", self.EASY
        yield "MEDIUM", self.MEDIUM
        yield "HARD", self.HARD

    def __repr__(self):
        return "WMLValue({!r}, {!r}, {!r})".format(self.EASY, self.MEDIUM, self.HARD)


def tokenize(str):
    str = re.sub(r"\"\s*\+\s*\{([^}]*)\}\s*\+\s*_?\"", "\\1", str)
    str = re.sub(r"\{([^}]*)\}\s*\+\s*_?\"", "\"\\1", str)
    str = re.sub(r"\"\s*\+\s*\{([^}]*)\}", "\\1\"", str)
    while str:
        for type, regex in wml_regexes:
            m = regex.match(str)
            if m:
                groups = m.groups()
                str = str[m.end():]
                if type in ("whitespace", "comment"):
                    pass
                elif type == "keys":
                    yield from (("key", x) for x in zip(groups[0].split(","),
                                                        groups[1].split(",")))
                elif type == "macro_open":
                    contents = groups[0]
                    count = 1
                    while count:
                        c = str[0]
                        if c == "{":
                            count += 1
                        if c == "}":
                            count -= 1
                        contents += c
                        str = str[1:]
                    yield "macro", (contents[1:-1],)
                else:
                    yield type, groups
                break
        else:
            raise RuntimeError("Can't parse {}".format(repr(str[:100])))


def parse_wml(tokens, tag_ann="all"):
    keys = collections.defaultdict(WMLValue)
    tags = collections.defaultdict(list)
    macros = []
    annotation = levels
    tokens = iter(tokens)
    for type, value in tokens:
        if type == "key":
            name, value = value
            if name == "increse_attacks":
                name = "increase_attacks"
            for l in annotation:
                setattr(keys[name], l, value)
        if type == "open":
            subtokens = []
            nt = next(tokens)
            count = 0
            while count or nt != ("close", (value[0],)):
                if nt == ("open", (value[0],)):
                    count += 1
                if nt == ("close", (value[0],)):
                    count -= 1
                subtokens.append(nt)
                nt = next(tokens)
            try:
                tag = parse_wml(subtokens, annotation)
            except Exception as e:
                print(e.__class__)
                raise
            else:
                tags[value[0]].append(tag)
        if type == "close":
            pass
        if type == "macro":
            if value[0].startswith("QUANTITY "):
                _, name, easy, medium, hard = value[0].split()
                keys[name].EASY = easy
                keys[name].MEDIUM = medium
                keys[name].HARD = hard
            else:
                macros.append(value[0])
        if type == "pre":
            if value[0] == "ifdef":
                annotation = {value[1]}
            elif value[0] == "else":
                annotation = set(levels) - annotation
            elif value[0] == "endif":
                annotation = levels
            elif value[0] == "define":
                name = value[1].split()[0]
                subtokens = []
                nt = next(tokens)
                while nt != ("pre", ("enddef", "")):
                    subtokens.append(nt)
                    nt = next(tokens)
                tag = parse_wml(subtokens, annotation)
                tags[name].append(tag)
    return WMLTag(keys, tags, tag_ann, macros)


def format_parsed(tag, level=0):
    stuff = []
    for key, values in tag.keys.items():
        if values.all():
            stuff.append("    " * level + key + " = " + values.any)
        else:
            for name, value in values.iter():
                stuff.append("    " * level + key + " = " + value + " # " + name)
    for name, tags in tag.tags.items():
        for tag in tags:
            if tag.annotation != "all":
                stuff.append("    " * level + "# " + tag.annotation)
            stuff.append("    " * level + "[{}]".format(name))
            stuff.append(format_parsed(tag, level=level + 1))
            stuff.append("    " * level + "[/{}]".format(name))
    return "\n".join(stuff)


def extract_abilities(start):
    data = (start / "utils" / "abilities.cfg").open().read()
    for type, obj in re.findall(r"#define ((?:ABILITY|WEAPON_SPECIAL)\S+)[^\n]*(.*?)#enddef", data, re.DOTALL):
        try:
            stuff = parse_wml(tokenize(obj))
        except RuntimeError as e:
            print(obj, "X" * 50, obj, "X" * 50, e, type(e))
            raise e
        for tag_type, tags in stuff.tags.items():
            for tag in tags:
                if tag.keys["name"].any:
                    if type.startswith("ABILITY"):
                        section = "Abilities"
                    else:
                        section = "Weapon Specials"
                    yield section, tag.keys["name"].any, tag_type, type, tag
                    #print(tag_type, tag.keys["name"].any)
                    #print(tag_type, tag.keys["description"].any)
        #stuff = stuff.tags["object"][0]
        #if "name" in stuff.keys and "filter" not in stuff.tags:
            #yield stuff.keys["name"].any, stuff


def extract_items(start):
    data = (start / "utils" / "item_list.cfg").open().read()
    for obj in re.findall(r"\[object\].*?\[/object\]", data, re.DOTALL):
        try:
            stuff = parse_wml(tokenize(obj))
        except RuntimeError as e:
            print(f, "X" * 50, obj, "X" * 50, e, type(e))
            raise e
        stuff = stuff.tags["object"][0]
        if "name" in stuff.keys and "filter" not in stuff.tags:
            yield stuff.keys["name"].any, stuff


def extract_advancements(start):
    for item in start.iterdir():
        if item.is_dir():
            yield from extract_advancements(item)
        elif item.suffix == ".cfg":
            yield from extract_from_file(item)


def extract_utils_amla_advancements(fname):
    x = parse_wml(tokenize(fname.open().read()))
    for name, tags in x.tags.items():
        if name.endswith("ADVANCEMENTS") or name in ["ADDITIONAL_AMLA",
                                                     "LEGACY_DISCOVERY"]:
            if name == "LEGACY_DISCOVERY":
                name = "GENERIC_AMLA_ADVANCEMENTS"
            for adv in tags[0].tags["advancement"]:
                yield name, adv.keys["id"].any, adv


def extract_from_file(f):
    data = f.open().read()
    for adv in re.findall(r"\[advancement\].*?\[/advancement\]", data, re.DOTALL):
        try:
            stuff = parse_wml(tokenize(adv))
        except RuntimeError as e:
            print(f, "X" * 50, adv, "X" * 50, e, type(e))
            raise e
        stuff = stuff.tags["advancement"][0]
        if "id" not in stuff.keys:
            print(stuff.keys.keys())
        yield f.stem, stuff.keys["id"].any, stuff


def format_values(values, positive, negative="",
                  percent=False, sort=False, invert=False):
    s = ""
    reverse = (all(i in "1234567890-" for n, v in values.iter() for i in v)
               and values.any and (int(values.any) < 0 or invert))
    for name, value in ([("", values.any)] if values.all else values.iter()):
        if not value:
            continue
        if s:
            s += ", "
        if sort and value in sort_translations:
            s += str(sort_translations[value])
        elif reverse:
            s += str(-int(value))
        else:
            s += str(value)
        if percent:
            s += "%"
        if not values.all:
            s += " (in {} difficulty)".format(name)
    if reverse:
        return negative.format(s)
    return positive.format(s)


def item_key_sort(kv):
    key, values = kv
    if key == "sort":
        return 0
    if key == "damage":
        return 1
    if key == "attacks":
        return 2
    if key == "defence":
        return 3
    if key.endswith("resist"):
        return 9
    if key.endswith("penetrate"):
        return 10
    else:
        return 11


def special_name(index, name, args):
    if name in special_translation:
        x = special_translation[name]
    elif name == "PLAGUE_TYPE_LOTI":
        x = "plague ({})".format(args[0])
    elif name == "LESSER_BERSERK":
        x = "lesser berserk ({})".format(args[0])
    elif name == "EXTRA_5_IMPACT_DAMAGE":
        x = "extra damage (+5; impact)"
    else:
        x = name.replace("_", " ").lower()
    if "WEAPON_SPECIAL_" + name in index:
        return "[[LotI_Abilities#{}|{}]]".format(index["WEAPON_SPECIAL_" + name], x)
    return x


def ability_name(index, name, args):
    args = [i.replace("_", "") for i in args]
    if name in ability_translation:
        x = ability_translation[name]
    elif name.startswith("ABSORB"):
        x = "absorbs ({})".format(name.replace("ABSORB_", ""))
    elif name == "INCREASE_RESISTANCE_AURA":
        x = "{} ({})".format(*args)
    elif name == "EXTRA_DAMAGE_AURA":
        x = "{} ({})".format(*args)
    elif name == "BURNING_AURA":
        x = "burns foes ({})".format(args[0])
    elif name == "REGENERATES_OTHER":
        x = "regenerates ({})".format(args[0])
    elif name == "HEALS_OTHER":
        x = "heals ({})".format(args[0])
    elif name in ("SHIELD", "DESPAIR", "CONVICTION", "FRAIL_TIDE", "UNHOLYBANE"):
        x = "{} ({})".format(name.replace("_", " ").lower(), args[0])
    else:
        x = name.replace("_", " ").lower()
    if "ABILITY_" + name in index:
        return "[[LotI_Abilities#{}|{}]]".format(index["ABILITY_" + name], x)
    return x


class make_adv_index:
    def __init__(self, advs):
        self.index = {}
        self.refs = set()
        for section, name, tag in advs:
            iname = name
            ref = "{}_.E2.80.93_{}".format(tag.keys["description"].any.replace(" ", "_"),
                                           iname.replace(" ", "_"))
            i = 2
            while ref in self.refs:
                iname = "{}_{}".format(name, i)
                ref = "{}_.E2.80.93_{}".format(tag.keys["description"].any.replace(" ", "_"),
                                               iname.replace(" ", "_"))
                i += 1
            self.refs.add(ref)
            self.index[section + name] = ref
            if name in self.index:
                self.index[name] = ""
            else:
                self.index[name] = ref

    def get(self, section, id):
        if id in self.index and self.index[id]:
            return self.index[id]
        if section + id in self.index:
            return self.index[section + id]
        return ""


def make_item_index(items):
    index = {}
    for name, tag in items:
        index[name.lower()] = "{}_.E2.80.93_{}".format(name, format_values(tag.keys["sort"],
                                                                           "{}", sort=True))
    index["dark sword of desctruction"] = index["dark sword of destruction"]
    index["dark helm of desctruction"] = index["dark helm of destruction"]
    return index


def make_ability_index(items):
    index = {}
    for section, name, type, macro_name, tag in items:
        index[macro_name] = "{}_.E2.80.93_{}".format(name, type)
    return index


def write_item(name, tag, file, index):
    def print(*a, **kw):
        k = {"file": file, "end": "<br/>\n"}
        k.update(kw)
        return builtins.print(*a, **k)
    sort = tag.keys["sort"].any
    keys = tag.keys
    print("===", name, "&ndash;", format_values(keys["sort"], "{}", sort=True), "===", end="\n")
    if "flavour" in keys:
        print("<span style='color:#808080'><i>{}</i></span>".format(keys["flavour"].any))
    if "defence" in keys:
        print(format_values(keys["defence"],
                            "<span style='color:#60A0FF'>Increases physical resistances by {}</span>",
                            "<span style='color:#60A0FF'>Decreases physical resistances by {}</span>",
                            percent=True))
    for t in damage_ranges:
        e = " ({} attacks only)".format(t.replace("_", "")) if t else t
        if "damage" + t in keys:
            print(format_values(keys["damage" + t],
                                "<span style='color:green'>Damage increased by {{}}{}</span>".format(e),
                                "<span style='color:green'>Damage decreased by {{}}{}</span>".format(e),
                                percent=True))
        if "damage" + t + "_plus" in keys:
            print(format_values(keys["damage" + t + "_plus"],
                                "<span style='color:green'>Damage increased by {{}}{}</span>".format(e),
                                "<span style='color:green'>Damage decreased by {{}}{}</span>".format(e)))
    if "attacks" in keys:
        print(format_values(keys["attacks"],
                            "<span style='color:green'>{} more attacks</span>",
                            "<span style='color:green'>{} fewer attacks</span>",
                            percent=sort in weapon_sorts))
    if "merge" in keys:
        print("<span style='color:green'>Merges attacks</span>")
    if "damage_type" in keys:
        print(format_values(keys["damage_type"],
                            "<span style='color:green'>Sets damage type to {}</span>"))
    if "suck" in keys:
        print(format_values(keys["suck"],
                            "<span style='color:#60A0FF'>Sucks {} health from targets with each hit</span>"))
    if "spell_suck" in keys:
        print(format_values(keys["spell_suck"],
                            "<span style='color:#60A0FF'>Spells suck {} health from targets with each hit</span>"))
    if "devastating_blow" in keys:
        print(format_values(keys["devastating_blow"],
                            "<span style='color:#60A0FF'>{} chance to strike a devastating blow</span>",
                            percent=True))
    for t in damage_types:
        if t + "_penetrate" in keys:
            print(format_values(keys[t + "_penetrate"],
                                "<span style='color:green'>Enemy resistances to {} decreased by {{}}</span>".format(t),
                                percent=True))
    for t in damage_ranges:
        e = " ({} attacks only)".format(t.replace("_", "")) if t else t
        for specials in tag.tags["specials" + t]:
            for special in specials.macros:
                name, *args = shlex.split(special)
                real_name = special_name(name.replace("WEAPON_SPECIAL_", ""), args)
                print("<span style='color:green'>New weapon special: {}{}</span>".format(real_name, e))
    if "magic" in keys:
        print(format_values(keys["magic"],
                            "<span style='color:green'>Increases all magical damages by {}</span>",
                            percent=True))
    if "dodge" in keys:
        print(format_values(keys["dodge"],
                            "<span style='color:#60A0FF'>Chance to get hit decreased by {}</span>",
                            percent=True))
    for t in damage_types:
        if t + "_resist" in keys:
            print(format_values(keys[t + "_resist"],
                                "<span style='color:#60A0FF'>Resistance to {} increased by {{}}</span>".format(t),
                                "<span style='color:#60A0FF'>Resistance to {} decreased by {{}}</span>".format(t),
                                percent=True))
    for effect in tag.tags["effect"]:
        if effect.keys["apply_to"].any == "new_ability":
            for specials in effect.tags["abilities"]:
                for special in specials.macros:
                    name, *args = shlex.split(special)
                    real_name = ability_name(name.replace("ABILITY_", ""), args)
                    print("<span style='color:#60A0FF'>New ability: {}</span>".format(real_name))
        elif effect.keys["apply_to"].any == "movement":
            print(format_values(effect.keys["increase"],
                                "<span style='color:#60A0FF'>{} more movement points</span>",
                                "<span style='color:#60A0FF'>{} fewer movement points</span>"))
        elif effect.keys["apply_to"].any == "vision":
            print(format_values(effect.keys["vision"],
                                "<span style='color:#60A0FF'>Increases vision range by {}</span>",
                                "<span style='color:#60A0FF'>Decreases vision range by {}</span>"))
        elif effect.keys["apply_to"].any == "hitpoints":
            times = " per level" if "times" in effect.keys and effect.keys["times"].any == "per level" else ""
            if "increase_total" in effect.keys:
                print(format_values(effect.keys["increase_total"],
                                    "<span style='color:#60A0FF'>{{}} more hitpoints per level{}</span>".format(times),
                                    "<span style='color:#60A0FF'>{{}} fewer hitpoints per level{}</span>".format(times)))
            if "heal_full" in effect.keys and effect.keys["heal_full"].any == "yes":
                print("<span style='color=#60A0FF'>Full heal</span>")
        elif effect.keys["apply_to"].any == "defence":
            for defence in effect.tags["defence"]:
                for m, h in defence_types:
                    if m in defence.keys:
                        print(format_values(defence.keys[m],
                                            "<span style='color:#60A0FF'>Chance to get hit {} increased by {{}}</span>".format(h),
                                            "<span style='color:#60A0FF'>Chance to get hit {} reduced by {{}}</span>".format(h),
                                            percent=True))
        elif effect.keys["apply_to"].any == "movement_costs":
            for movement in effect.tags["movement_costs"]:
                for m, h in movement_costs:
                    if m in movement.keys:
                        print(format_values(movement.keys[m],
                                            "<span style='color:#60A0FF'>Movement costs {} set to {{}}</span>".format(h)))
        elif effect.keys["apply_to"].any == "alignment":
            print(format_values(effect.keys["alignment"],
                                "<span style='color:#60A0FF'>Sets alignment to {}</span>"))
        elif effect.keys["apply_to"].any == "status" and effect.keys["add"].any == "not_living":
            print("<span style='color:#60A0FF'>Unlife (immunity to poison, plague and drain)</span>")
        elif effect.keys["apply_to"].any == "new_attack":
            print("<span style='color:green'>New attack: {} ({} - {})</span>".format(effect.keys["name"].any,
                                                                                     effect.keys["damage"].any,
                                                                                     effect.keys["number"].any))
        elif effect.keys["apply_to"].any == "new_advancement":
            print("<span style='color:yellow'>New advancements: {}</span>".format(effect.keys["description"].any))
        elif effect.keys["apply_to"].any in ["attack", "improve_bonus_attack"]:
            bonus = effect.keys["apply_to"].any == "improve_bonus_attack"
            range = wname = wtype = rt = ""
            if "range" in effect.keys:
                range = format_values(effect.keys["range"], "{}")
            if "type" in effect.keys:
                wtype = format_values(effect.keys["type"], "{}")
            if range or wtype:
                rt = " ({}{}{} attacks only)".format(range, " " if range and wtype else "", wtype)
            if "name" in effect.keys:
                wname = format_values(effect.keys["name"], " for the {} attack")
            for specials in effect.tags["set_specials"]:
                for special in specials.macros:
                    name, *args = shlex.split(special)
                    real_name = special_name(name.replace("WEAPON_SPECIAL_", ""), args)
                    print("<span style='color:green'>New weapon special{}{}: {}</span>".format(wname, rt, real_name))
            if "remove_specials" in effect.keys:
                print(format_values(effect.keys["remove_specials"],
                                    "<span style='color:green'>Remove weapon special{}{}: {{}}</span>".format(wname, rt)))
            if "increase_damage" in effect.keys:
                print(format_values(effect.keys["increase_damage"],
                                    "<span style='color:green'>Damage increased by {{}}{}{}</span>".format(wname, rt),
                                    "<span style='color:green'>Damage decreased by {{}}{}{}</span>".format(wname, rt),
                                    percent=bonus))
            if "increase_attacks" in effect.keys:
                print(format_values(effect.keys["increase_attacks"],
                                    "<span style='color:green'>{{}} more attacks{}{}</span>".format(wname, rt),
                                    "<span style='color:green'>{{}} fewer attacks{}{}</span>".format(wname, rt)))
            if "set_type" in effect.keys:
                print(format_values(effect.keys["set_type"],
                                    "<span style='color:green'>Sets damage type to {{}}{}{}</span>".format(wname, rt)))

    for latent in tag.tags["latent"]:
        value = re.sub("\(requires ([^)]+)\)",
                       lambda m: "(requires [[#{}|{}]])".format(index[m.group(1).lower()], m.group(1)),
                       latent.keys["desc"].any)
        print(re.sub("color='([^']+)'", "style='color:\\1'", value))
    if "description" in keys:
        v = re.sub("color='([^']+)'", "style='color:\\1'", keys["description"].any)
        if "<" not in v:
            v = "<span style='color:#808080'><i>{}</i></span>".format(v)
        print(v)
    print()


def write_advancement(section, name, tag, file, index):
    def print(*a, **kw):
        k = {"file": file, "end": "<br/>\n"}
        k.update(kw)
        return builtins.print(*a, **k)
    keys = tag.keys
    print("===", keys["description"].any, "&ndash;", name, "===", end="\n")
    if "max_times" in keys:
        print("<span style='color:#808080'><i>This advancement can be taken {} times</i></span>".format(keys["max_times"].any))
    if "require_amla" in keys and keys["require_amla"].any not in ["{LEGACY}", ""]:
        amlas = [n.strip() for n in keys["require_amla"].any.split(",")]
        amlas = ", ".join("[[#{}|{}]]".format(index.get(section, n), n) if index.get(section, n) != "" else n for n in amlas)
        print("<span style='color:#808080'><i>This advancement requires the advancements {} to be achieved first</i></span>".format(amlas))
    for effect in tag.tags["effect"]:
        if effect.keys["apply_to"].any == "new_ability":
            for specials in effect.tags["abilities"]:
                for special in specials.macros:
                    name, *args = shlex.split(special)
                    real_name = ability_name(name.replace("ABILITY_", ""), args)
                    print("<span style='color:#60A0FF'>New ability: {}</span>".format(real_name))
        elif effect.keys["apply_to"].any == "remove_ability":
            for specials in effect.tags["abilities"]:
                for special in specials.macros:
                    name, *args = shlex.split(special)
                    real_name = ability_name(name.replace("ABILITY_", ""), args)
                    print("<span style='color:#60A0FF'>Remove ability: {}</span>".format(real_name))
        elif effect.keys["apply_to"].any == "movement":
            print(format_values(effect.keys["increase"],
                                "<span style='color:#60A0FF'>{} more movement points</span>",
                                "<span style='color:#60A0FF'>{} fewer movement points</span>"))
        elif effect.keys["apply_to"].any == "vision":
            print(format_values(effect.keys["vision"],
                                "<span style='color:#60A0FF'>Increases vision range by {}</span>",
                                "<span style='color:#60A0FF'>Decreases vision range by {}</span>"))
        elif effect.keys["apply_to"].any == "hitpoints":
            times = " per level" if "times" in effect.keys and effect.keys["times"].any == "per level" else ""
            if "increase_total" in effect.keys:
                print(format_values(effect.keys["increase_total"],
                                    "<span style='color:#60A0FF'>{{}} more hitpoints per level{}</span>".format(times),
                                    "<span style='color:#60A0FF'>{{}} fewer hitpoints per level{}</span>".format(times)))
            if "heal_full" in effect.keys and effect.keys["heal_full"].any == "yes":
                print("<span style='color=#60A0FF'>Full heal</span>")
        elif effect.keys["apply_to"].any == "defence":
            for defence in effect.tags["defence"]:
                for m, h in defence_types:
                    if m in defence.keys:
                        print(format_values(defence.keys[m],
                                            "<span style='color:#60A0FF'>Chance to get hit {} increased by {{}}</span>".format(h),
                                            "<span style='color:#60A0FF'>Chance to get hit {} reduced by {{}}</span>".format(h),
                                            percent=True))
        elif effect.keys["apply_to"].any == "movement_costs":
            for movement in effect.tags["movement_costs"]:
                for m, h in movement_costs:
                    if m in movement.keys:
                        print(format_values(movement.keys[m],
                                            "<span style='color:#60A0FF'>Movement costs {} set to {{}}</span>".format(h)))
        elif effect.keys["apply_to"].any == "alignment":
            print(format_values(effect.keys["alignment"],
                                "<span style='color:#60A0FF'>Sets alignment to {}</span>"))
        elif effect.keys["apply_to"].any == "status" and effect.keys["add"].any == "not_living":
            print("<span style='color:#60A0FF'>Unlife (immunity to poison, plague and drain)</span>")
        elif effect.keys["apply_to"].any == "new_attack":
            print("<span style='color:green'>New attack: {} ({} - {})</span>".format(effect.keys["name"].any,
                                                                                     effect.keys["damage"].any,
                                                                                     effect.keys["number"].any))
        elif effect.keys["apply_to"].any == "new_advancement":
            print("<span style='color:yellow'>New advancements: {}</span>".format(effect.keys["description"].any))
        elif effect.keys["apply_to"].any in ["attack", "improve_bonus_attack"]:
            bonus = effect.keys["apply_to"].any == "improve_bonus_attack"
            range = wname = wtype = rt = ""
            if "range" in effect.keys:
                range = format_values(effect.keys["range"], "{}")
            if "type" in effect.keys:
                wtype = format_values(effect.keys["type"], "{}")
            if range or wtype:
                rt = " ({}{}{} attacks only)".format(range, " " if range and wtype else "", wtype)
            if "name" in effect.keys:
                wname = format_values(effect.keys["name"], " for the {} attack")
            for specials in effect.tags["set_specials"]:
                for special in specials.macros:
                    name, *args = shlex.split(special)
                    real_name = special_name(name.replace("WEAPON_SPECIAL_", ""), args)
                    print("<span style='color:green'>New weapon special{}{}: {}</span>".format(wname, rt, real_name))
            if "remove_specials" in effect.keys:
                print(format_values(effect.keys["remove_specials"],
                                    "<span style='color:green'>Remove weapon special{}{}: {{}}</span>".format(wname, rt)))
            if "increase_damage" in effect.keys:
                print(format_values(effect.keys["increase_damage"],
                                    "<span style='color:green'>Damage increased by {{}}{}{}</span>".format(wname, rt),
                                    "<span style='color:green'>Damage decreased by {{}}{}{}</span>".format(wname, rt),
                                    percent=bonus))
            if "increase_attacks" in effect.keys:
                print(format_values(effect.keys["increase_attacks"],
                                    "<span style='color:green'>{{}} more attacks{}{}</span>".format(wname, rt),
                                    "<span style='color:green'>{{}} fewer attacks{}{}</span>".format(wname, rt)))
            if "set_type" in effect.keys:
                print(format_values(effect.keys["set_type"],
                                    "<span style='color:green'>Sets damage type to {{}}{}{}</span>".format(wname, rt)))
        elif effect.keys["apply_to"].any == "resistance":
            for resistances in effect.tags["resistance"]:
                for t in damage_types:
                    if t in resistances.keys:
                        print(format_values(resistances.keys[t],
                                            "<span style='color:#60A0FF'>Resistance to {} increased by {{}}</span>".format(t),
                                            "<span style='color:#60A0FF'>Resistance to {} decreased by {{}}</span>".format(t),
                                            percent=True, invert=True))
        elif effect.keys["apply_to"].any == "defense":
            for defence in effect.tags["defense"]:
                for m, h in defence_types:
                    if m in defence.keys:
                        print(format_values(defence.keys[m],
                                            "<span style='color:#60A0FF'>Chance to get hit {} increased by {{}}</span>".format(h),
                                            "<span style='color:#60A0FF'>Chance to get hit {} reduced by {{}}</span>".format(h),
                                            percent=True))
        elif effect.keys["apply_to"].any == "bonus_attack":
            print("<span style='color:green'>New bonus attack: {} ({}% - {} {})</span>".format(effect.keys["name"].any,
                                                                                               effect.keys["damage"].any,
                                                                                               effect.keys["range"].any,
                                                                                               effect.keys["type"].any))
            wname = format_values(effect.keys["name"], " for the {} attack")
            for specials in effect.tags["specials"]:
                for special in specials.macros:
                    name, *args = shlex.split(special)
                    real_name = special_name(name.replace("WEAPON_SPECIAL_", ""), args)
                    print("<span style='color:green'>New weapon special{}: {}</span>".format(wname, real_name))
    print()



def write_ability(section, name, type, macro_name, tag, file):
    def print(*a, **kw):
        k = {"file": file, "end": "<br/>\n"}
        k.update(kw)
        return builtins.print(*a, **k)
    keys = tag.keys
    print("===", keys["name"].any, "&ndash;", type, "===", end="\n")
    print(re.sub("([^\n]+)", "<span style='color:#808080'><i>\\1</i></span>", keys["description"].any))
    print()


def main():
    global ability_name, special_name

    parser = argparse.ArgumentParser()
    parser.add_argument("LoTIdir")

    args = parser.parse_args()

    start = pathlib.Path(args.LoTIdir).resolve()
    print("LotI Scraper version", __version__, "loading from directory", start)

    print("Scanning info...")
    info = parse_wml(tokenize((start / "_info.cfg").open().read()))
    version = info.tags["info"][0].keys["version"].any

    print("LotI version is", version)

    def sort_by_first(x):
        if x[0] == "GENERIC_AMLA_ADVANCEMENTS":
            return "0" + x[0]
        if x[0] == "ADDITIONAL_AMLA":
            return "1" + x[0]
        if x[0][0] == "S":
            return "2" + x[0]
        return "3" + x[0]

    def sort_by_not_last(x):
        return x[:-1]

    print("Scanning standard advancements...")
    advancements_standard = list(extract_utils_amla_advancements(start / "utils" / "amla.cfg"))
    advancements_standard.sort(key=sort_by_first)

    print("Scanning unit advancements...")
    advancements_units = list(extract_advancements(start / "units"))
    advancements_units.sort(key=sort_by_first)

    print("Scanning abilities...")
    abilities = list(extract_abilities(start))
    abilities.sort(key=sort_by_not_last)
    ability_index = make_ability_index(abilities)

    def ability_name(*args, old=ability_name):
        return old(ability_index, *args)


    def special_name(*args, old=special_name):
        return old(ability_index, *args)


    print("Scanning items...")
    items = list(extract_items(start))
    items.sort(key=sort_by_not_last)

    print("Found", len(abilities), "abilities,",len(advancements_standard), "standard advancements,",
          len(advancements_units), "unit advancements and", len(items), "items")

    print("Writing item information to items.wiki")
    with open("items.wiki", "w") as items_file:
        print(header.format("all the items", "", version), file=items_file)

        index = make_item_index(items)
        for item in items:
            write_item(*item, items_file, index)

    print("Writing ability information to abilities.wiki")
    with open("abilities.wiki", "w") as ability_file:
        print(header.format("all the abilities and weapon specials", "", version), file=ability_file)

        #index = make_item_index(items)
        for section, abilities in itertools.groupby(abilities, lambda x: x[0]):
            print("==", section, "==", file=ability_file)
            for ab in abilities:
                write_ability(*ab, ability_file)

    print("Writing standard advancement information to advancements_standard.wiki")
    with open("advancements_standard.wiki", "w") as adv_standard_file:
        print(header.format("all the advancements avalible for catagories of units",
                            "See https://wiki.wesnoth.org/LotI_Unit_Advancements for unit specific advancements.",
                            version), file=adv_standard_file)

        index = make_adv_index(advancements_standard)
        for section, advs in itertools.groupby(advancements_standard, sort_by_first):
            section = section[1:]
            if section == "GENERIC_AMLA_ADVANCEMENTS":
                section = "Legacies and Books"
            elif section == "ADDITIONAL_AMLA":
                section = "Soul Eater and God Advancements"
            else:
                section = section.replace("_", " ").replace("AMLA ", "").title()
            print("==", section, "==", file=adv_standard_file)
            print(file=adv_standard_file)
            for adv in advs:
                write_advancement(*adv, adv_standard_file, index)
            print(file=adv_standard_file)

    print("Writing unit advancement information to advancements_units.wiki")
    with open("advancements_units.wiki", "w") as adv_units_file:
        print(header.format("all the advancements that are unit specific",
                            "See https://wiki.wesnoth.org/LotI_Standard_Advancements for general advancements such as legacies and books.",
                            version), file=adv_units_file)

        index = make_adv_index(advancements_units)
        for section, advs in itertools.groupby(advancements_units, sort_by_first):
            section = section[1:].replace("_", " ").replace("AMLA ", "").title()
            print("==", section, "==", file=adv_units_file)
            print(file=adv_units_file)
            for adv in advs:
                write_advancement(*adv, adv_units_file, index)
            print(file=adv_units_file)

    print("All done!")


if __name__ == "__main__":
    main()
@Dugi
Widowmaker, Widowermaker:
The number of widows will raise after this battle.
Raise to rise maybe?
Also, did you see viewtopic.php?f=23&t=32370 as some of those may be useful, due to the amount of magic / magic-based attacks in LotI
Wussel
Posts: 623
Joined: July 28th, 2012, 5:58 am

Re: Legend of the Invincibles

Post by Wussel »

I have an idea for a new class of specials.

Something like a goblin whip master. But essentially could be any drummer or such. Idea is that they give additional movement to the units around them at the beginning of the turn or at the end for the next turn. Maybe even 3 levels are in order (+1,+2,+3). That way you could get slow units through a fjord, snow or a cave.

Just dropping it here because you are the one who could make such a thing.
User avatar
matsjoyce
Posts: 233
Joined: May 8th, 2011, 2:10 pm
Location: UK

Re: Legend of the Invincibles

Post by matsjoyce »

Image
"They run as if the very whips of their master were behind them."
:hmm: Might work. The extra moves should be added at the start of the turn IMO. It would be like a springboard for attacking faster. Would be good for HI crossing a stream...
User avatar
Dugi
Posts: 4960
Joined: July 22nd, 2010, 10:29 am
Location: Carpathian Mountains
Contact:

Re: Legend of the Invincibles

Post by Dugi »

@matsjoyce
Thanks for the good work. I have updated the initial post with the new page and the new version of the script.

@Wussel
It might have a tactical effect and it should be trivial to code, but its theme sounds a bit awkward. The heroes advocate the use of undead in battle instead of normal fighters because they do not suffer, whipping their soldiers does not sound like something they would do. Can you please come up with a better explanation what makes your own units run like if hellhounds were on their trails? Maybe that the unit with the ability is so horrifying that its allies prefer to flee (Efraim and Lethalia, incarnations of grim stygian darkness, would not notice)?
User avatar
nuorc
Forum Regular
Posts: 569
Joined: September 3rd, 2009, 2:25 pm
Location: Barag Gor

Re: Legend of the Invincibles

Post by nuorc »

Dugi wrote:its theme sounds a bit awkward
Wussel wrote:essentially could be any drummer or such
I think there's nothing wrong with the drummer theme; maybe call it 'marching drums' or something.
Spoiler:
I have a cunning plan.
Delicius169
Posts: 179
Joined: February 16th, 2015, 5:02 pm

Re: Legend of the Invincibles

Post by Delicius169 »

I like the goblin suggestion.
I think it would be nice, if every race captain had one ability, noone else would.
For example Elvish Marshal could have aura of strugle, as elves are more nimble based unit, then resistace based.
Lich king could have something like aura of sucking +1 suck/or aura of leechces
Dwarf king could have the Hate speech
And The goblin can have marching drums...
User avatar
Dugi
Posts: 4960
Joined: July 22nd, 2010, 10:29 am
Location: Carpathian Mountains
Contact:

Re: Legend of the Invincibles

Post by Dugi »

Okay, so it shall be the drummer.
I wrote:The investigation of the save file of a leader not attacking even a completely vulnerable enemy has shown that it was an old save file that did not have the custom AI enabled. Therefore, it would be helpful to me if someone attached a save file where a leader does not attack an unprotected unit (he should not rush into a group of your units)?
This issue is gone?
User avatar
Eagle_11
Posts: 759
Joined: November 20th, 2013, 12:20 pm

Re: Legend of the Invincibles

Post by Eagle_11 »

Wussel wrote:I have an idea for a new class of specials.

Something like a goblin whip master. But essentially could be any drummer or such. Idea is that they give additional movement to the units around them at the beginning of the turn or at the end for the next turn. Maybe even 3 levels are in order (+1,+2,+3). That way you could get slow units through a fjord, snow or a cave.

Just dropping it here because you are the one who could make such a thing.
I had found something similar to that.
My usage for it was intended for Grand Knight, to give the adjacent half of their maximum movement as temporary extra moves at turn begin, but only for other loyalist cavalry types. I have not edited it yet to become as such, for now it just gives +2 movement to any adjacent type. Take use if you want.

Code: Select all

#define ABILITY_CUSTOM_INSPIRATION
[dummy]
    id=custom_inspiration
    name= _ "inspiration"
    description=  _ "inspires adjacent units to move 2 hex further."
[/dummy]
[/abilities]
    [event]
        name=turn refresh # After the normal restoration of movement
        first_time_only=no
        id=custom_inspiration_movement_event
        [store_unit]
            [filter]
                side=$side_number
                [filter_adjacent] # Grab all units standing next to units with inspiration
                    ability=custom_inspiration
                    [filter_side] # Don't get inspired by enemies
                        is_enemy=no
                    [/filter_side]
                [/filter_adjacent]
            [/filter]
            variable=inspired
        [/store_unit]
        {FOREACH inspired i} # Repeat this for every inspired unit
        {VARIABLE_OP inspired[$i].moves add 2} # Add 2 movement point to the moves restored to maximum
            [unstore_unit] # Return it back to the game
                variable=inspired[$i]
                find_vacant=no
            [/unstore_unit]
        {NEXT i}
        {CLEAR_VARIABLE inspired} # Clean up
    [/event]
[+abilities]
#enddef
In German the guy could be called 'Tambour', didn't find a translation...
Trommler for unit and Kriegstrommeln for ability ?
User avatar
firefox
Posts: 121
Joined: September 7th, 2009, 6:26 am
Location: Berlin

Re: Legend of the Invincibles

Post by firefox »

Eagle_11 wrote: Trommler for unit and Kriegstrommeln for ability ?
to me "Tambour" sounds better than "Trommler" ...

or you could call it a "Goblin Cheerleader" :P
i'm sure this would be pretty cute :3
may the source be with you
=(^.^)= nyan~
Post Reply