maduck/GoWDiscordTeamBot

View on GitHub
search.py

Summary

Maintainability
F
1 wk
Test Coverage
import calendar
import copy
import datetime
import importlib
import json
import logging
import operator
import re
from collections import defaultdict

import translations
from configurations import CONFIG
from data_source.game_data import GameData
from game_constants import COLORS, EVENT_TYPES, GEM_TUTORIAL_IDS, RARITY_COLORS, SOULFORGE_ALWAYS_AVAILABLE, \
    SOULFORGE_REQUIREMENTS, TROOP_RARITIES, \
    UNDERWORLD_SOULFORGE_REQUIREMENTS, WEAPON_RARITIES
from models.bookmark import Bookmark
from models.toplist import Toplist
from util import batched, dig, extract_search_tag, get_next_monday_in_locale, translate_day

WEEK_DAY_FORMAT = '%b %d'

LOGLEVEL = logging.DEBUG

formatter = logging.Formatter('%(asctime)-15s [%(levelname)s] %(message)s')
handler = logging.StreamHandler()
handler.setFormatter(formatter)
handler.setLevel(LOGLEVEL)
log = logging.getLogger(__name__)

log.setLevel(LOGLEVEL)
log.addHandler(handler)

t = translations.Translations()
_ = t.get


def update_translations():
    global _
    try:
        importlib.reload(translations)
        _ = translations.Translations().get
    except (NameError, json.decoder.JSONDecodeError):
        log.exception('Could not update translations, stacktrace follows.')


class TeamExpander:
    my_emojis = {}

    def __init__(self):
        world = GameData()
        world.populate_world_data()
        self.troops = world.troops
        self.troop_types = world.troop_types
        self.spells = world.spells
        self.effects = world.effects
        self.positive_effects = world.positive_effects
        self.weapons = world.weapons
        self.classes = world.classes
        self.banners = world.banners
        self.traits = world.traits
        self.kingdoms = world.kingdoms
        self.pet_effects = world.pet_effects
        self.pets = world.pets
        self.talent_trees = world.talent_trees
        self.spoilers = world.spoilers
        self.events = world.events
        self.campaign_week = world.campaign_week
        self.campaign_name = world.campaign_name
        self.campaign_tasks = world.campaign_tasks
        self.task_skip_costs = world.campaign_skip_costs
        self.reroll_tasks = world.campaign_rerolls
        self.soulforge = world.soulforge
        self.summons = world.summons
        self.traitstones = world.traitstones
        self.levels = world.levels
        self.rooms = {}
        self.toplists = Toplist()
        self.bookmarks = Bookmark()
        self.adventure_board = world.adventure_board
        self.drop_chances = world.drop_chances
        self.event_key_drops = world.event_chest_drops
        self.event_kingdoms = world.event_kingdoms
        self.weekly_event = world.weekly_event
        self.active_gems = world.gem_events
        self.store_data = world.store_data
        self.user_data = world.user_data
        self.hoard_potions = world.hoard_potions
        self.orbs = world.orbs

    @classmethod
    def extract_code_from_message(cls, raw_code):
        return [int(n.strip()) for n in raw_code.split(',') if n and n.isdigit()]

    def get_team_from_code(self, code, lang):
        result = {
            'troops': [],
            'banner': {},
            'class': None,
            'talents': [],
            'class_title': _('[CLASS]', lang),
            'troops_title': _('[TROOPS]', lang),
        }
        has_weapon = False
        has_class = False

        for i, element in enumerate(code):
            if troop := self.troops.get(element):
                troop = troop.copy()
                self.translate_troop(troop, lang)
                result['troops'].append(troop)
                continue

            if weapon := self.weapons.get(element):
                weapon = weapon.copy()
                self.translate_weapon(weapon, lang)
                result['troops'].append(weapon)
                has_weapon = True
                continue

            if _class := self.classes.get(element):
                result['class'] = _(_class['name'], lang)
                result['class_talents'] = _class['talents']
                has_class = True
                continue

            if banner := self.banners.get(element):
                result['banner'] = self.translate_banner(banner, lang)
                continue

            if 0 <= element <= 3:
                result['talents'].append(element)
                self.trim_talents(result)
                continue
            has_class = self.fill_up_troops_banner_and_class(i, result, has_class, lang)

        hero_present = has_weapon and has_class
        if hero_present:
            result['talents'] = self.populate_talents(result, lang)
        else:
            result['class'] = None
            result['talents'] = None

        return result

    def fill_up_troops_banner_and_class(self, i, result, has_class, lang):
        if i <= 3:
            result['troops'].append(self.troops['`?`'])
        elif i == 4:
            banner = {
                'colors': [('questionmark', 1)],
                'name': '[REQUIREMENTS_NOT_MET]',
                'filename': 'Locked',
                'id': '`?`'
            }
            result['banner'] = self.translate_banner(banner, lang)
        elif i == 12:
            result['class'] = _('[REQUIREMENTS_NOT_MET]', lang)
            result['talents'] = []
            return True
        return has_class

    @staticmethod
    def populate_talents(result, lang):
        new_talents = []
        for talent_no, talent_code in enumerate(result['talents']):
            talent = '-'
            if talent_code > 0:
                talent = _(result['class_talents'][talent_code - 1][talent_no]['name'], lang)
            new_talents.append(talent)
        return new_talents

    def trim_talents(self, result):
        if len(result['talents']) > 7:
            result['talents'] = result['talents'][-7:]

    def get_team_from_message(self, user_code, lang):
        if code := self.extract_code_from_message(user_code):
            return self.get_team_from_code(code, lang)
        else:
            return

    @staticmethod
    def search_item(search_term, lang, items, lookup_keys, translator, sort_by='name'):
        if search_term.startswith('#'):
            search_term = search_term[1:]
        if search_term.isdigit():
            if item := items.get(int(search_term)):
                result = item.copy()
                translator(result, lang)
                return [result]
            return []
        possible_matches = []
        real_search = extract_search_tag(search_term)
        if not real_search:
            return []
        for base_item in items.values():
            if base_item['name'] == '`?`' or base_item['id'] == '`?`':
                continue
            item = base_item.copy()
            translator(item, lang)
            lookups = {
                k: extract_search_tag(dig(item, k)) for k in lookup_keys
            }

            if real_search == extract_search_tag(item['name']):
                return [item]
            for key, lookup in lookups.items():
                if real_search in lookup:
                    possible_matches.append(item)
                    break

        return sorted(possible_matches, key=operator.itemgetter(sort_by))

    def search_troop(self, search_term, lang):
        lookup_keys = [
            'name',
            'kingdom',
            'type',
            'roles',
            'spell.description',
            'shiny',
        ]
        return self.search_item(search_term, lang,
                                items=self.troops,
                                lookup_keys=lookup_keys,
                                translator=self.translate_troop)

    def translate_troop(self, troop, lang):
        troop['name'] = _(troop['name'], lang, default=troop['reference_name'])
        troop['description'] = _(troop['description'], lang).replace('widerbeleben',
                                                                     'wiederbeleben')
        troop['color_code'] = "".join(troop['colors'])
        troop['rarity_title'] = _('[RARITY]', lang)
        troop['raw_rarity'] = troop['rarity']
        rarity_number = 1
        if troop['rarity'] in TROOP_RARITIES:
            rarity_number = TROOP_RARITIES.index(troop['rarity'])
        troop['rarity'] = _(f'[RARITY_{rarity_number}]', lang)
        troop['traits_title'] = _('[TRAITS]', lang)
        troop['traits'] = self.enrich_traits(troop['traits'], lang)
        troop['roles_title'] = _('[TROOP_ROLE]', lang)
        troop['roles'] = [_(f'[TROOP_ROLE_{role.upper()}]', lang) for role in troop['roles']]
        troop['type_title'] = _('[FILTER_TROOPTYPE]', lang)
        troop['raw_types'] = troop['types']
        types = [
            _(f'[TROOPTYPE_{_type.upper()}]', lang) for _type in troop['types']
        ]
        troop['type'] = ' / '.join(types)
        troop['kingdom_title'] = _('[KINGDOM]', lang)
        reference_name = troop['kingdom'].get('reference_name', troop['kingdom']['name'])
        troop['kingdom'] = _(troop['kingdom']['name'], lang)
        if self.is_untranslated(troop['kingdom']):
            troop['kingdom'] = reference_name
        troop['spell'] = self.translate_spell(troop['spell_id'], lang)
        troop['spell_title'] = _('[TROOPHELP_SPELL0]', lang)
        self.translate_traitstones(troop, lang)
        troop['bonuses_title'] = _('[BONUSES]', lang)
        if troop['has_shiny']:
            troop['shiny'] = _('[SHINY_LEVEL_HINT_FIND_TOKENS]', lang)
            troop['shiny_spell'] = self.translate_spell(troop['shiny_spell_id'], lang)

    @staticmethod
    def translate_traitstones(item, lang):
        item['traitstones_title'] = _('[SOULFORGE_TAB_TRAITSTONES]', lang)
        if 'traitstones' not in item:
            item['traitstones'] = []
        traitstones = [
            f'{_(rune["name"], lang)} ({rune["amount"]})'
            for rune in item['traitstones']
        ]
        item['traitstones'] = traitstones

    @staticmethod
    def enrich_traits(traits, lang, delve_id=None):
        new_traits = []
        for trait in traits:
            new_trait = trait.copy()
            new_trait['name'] = _(trait['name'], lang)
            new_trait['description'] = _(trait['description'], lang)
            if delve_id is not None:
                new_trait['description'] = _(f'[TREASURE_HOARD_POTION_DESC_{delve_id}]', lang)
            new_traits.append(new_trait)
        return new_traits

    def search_kingdom(self, search_term, lang):
        lookup_keys = ['name']
        return self.search_item(search_term, lang, items=self.kingdoms, lookup_keys=lookup_keys,
                                translator=self.translate_kingdom)

    def search_faction(self, search_term, lang):
        lookup_keys = ['name', 'translated_colors']
        items = {k: v for k, v in self.kingdoms.items() if v['underworld']}
        return self.search_item(search_term, lang, items=items, lookup_keys=lookup_keys,
                                translator=self.translate_kingdom)

    def kingdom_summary(self, lang):
        kingdoms = [k.copy() for k in self.kingdoms.values() if k['location'] == 'krystara' and len(k['colors']) > 0]
        for kingdom in kingdoms:
            self.translate_kingdom(kingdom, lang)
        return sorted(kingdoms, key=operator.itemgetter('name'))

    def translate_kingdom(self, kingdom, lang):
        kingdom['name'] = _(kingdom['name'], lang)
        if self.is_untranslated(kingdom['name']):
            kingdom['name'] = kingdom['reference_name']
        kingdom['description'] = _(kingdom['description'], lang)
        kingdom['punchline'] = _(kingdom['punchline'], lang)
        kingdom['troop_title'] = _('[TROOPS]', lang)

        kingdom['troops'] = []
        for troop_id in kingdom['troop_ids']:
            if troop_id not in self.troops:
                continue
            troop = self.troops[troop_id].copy()
            self.translate_troop(troop, lang)
            kingdom['troops'].append(troop)

        kingdom['troops'] = sorted(kingdom['troops'], key=operator.itemgetter('name'))
        kingdom['weapons_title'] = _('[WEAPONS:]', lang)
        kingdom['weapons'] = sorted([
            {'name': _(self.weapons[_id]['name'], lang),
             'id': _id
             } for _id in kingdom['weapon_ids']
        ], key=operator.itemgetter('name'))
        kingdom['banner_title'] = _('[BANNERS]', lang)
        kingdom['banner'] = self.translate_banner(self.banners[kingdom['id']], lang)

        kingdom['linked_kingdom'] = None
        if kingdom['linked_kingdom_id']:
            kingdom['linked_kingdom'] = _(self.kingdoms[kingdom['linked_kingdom_id']]['name'], lang)
        if kingdom['linked_kingdom'] and self.is_untranslated(kingdom['linked_kingdom']):
            kingdom['linked_kingdom'] = None
        kingdom['map'] = _('[MAPNAME_MAIN]', lang)
        kingdom['linked_map'] = _('[MAPNAME_UNDERWORLD]', lang)
        if kingdom['underworld']:
            kingdom['map'] = _('[MAPNAME_UNDERWORLD]', lang)
            kingdom['linked_map'] = _('[MAPNAME_MAIN]', lang)
        if 'primary_color' in kingdom:
            deed_num = COLORS.index(kingdom['primary_color'])
            kingdom['deed'] = _(f'[DEED{deed_num:02d}]', lang)
        color_emojis = [self.my_emojis.get(c) for c in kingdom['colors']]
        kingdom['color_emojis'] = "".join(color_emojis)
        kingdom['translated_colors'] = [_(f'[GEM_{c.upper()}]', lang) for c in kingdom['colors']]
        kingdom['color_title'] = _('[GEM_MASTERY]', lang)
        kingdom['stat_title'] = _('[STAT_BONUS]', lang)
        if 'class_id' in kingdom:
            kingdom['class_title'] = _('[CLASS]', lang)
            kingdom['class'] = _(self.classes[kingdom['class_id']]['name'], lang)
        if 'primary_stat' in kingdom:
            kingdom['primary_stat'] = _(f'[{kingdom["primary_stat"].upper()}]', lang)
        if 'pet' in kingdom:
            kingdom['pet_title'] = _('[PET_RESCUE_PET]', lang)
            kingdom['pet'] = kingdom['pet'].translations[lang]
        if 'event_weapon' in kingdom:
            kingdom['event_weapon_title'] = _('[FACTION_WEAPON]', lang)
            kingdom['event_weapon_id'] = kingdom['event_weapon']['id']
            event_weapon = kingdom['event_weapon'].copy()
            self.translate_weapon(event_weapon, lang)
            kingdom['event_weapon'] = event_weapon
        kingdom['max_power_level_title'] = _('[KINGDOM_POWER_LEVELS]', lang)

    def search_class(self, search_term, lang):
        lookup_keys = ['name']
        return self.search_item(search_term, lang,
                                items=self.classes,
                                translator=self.translate_class,
                                lookup_keys=lookup_keys)

    def class_summary(self, lang):
        classes = [c.copy() for c in self.classes.values()]
        for c in classes:
            self.translate_class(c, lang)
        return sorted(classes, key=operator.itemgetter('name'))

    def translate_class(self, _class, lang):
        kingdom = self.kingdoms[_class['kingdom_id']]
        _class['kingdom'] = _(kingdom['name'], lang, default=kingdom['reference_name'])
        weapon = self.weapons[_class['weapon_id']]
        _class['weapon'] = _(weapon['name'], lang)
        _class['name'] = _(_class['name'], lang)
        translated_trees = []
        for tree in _class['talents']:
            translated_talents = [
                {
                    'name': _(talent['name'], lang),
                    'description': _(talent['description'], lang),
                }
                for talent in tree
            ]
            translated_trees.append(translated_talents)
        self.translate_traitstones(_class, lang)
        _class['talents_title'] = _('[TALENT_TREES]', lang)
        _class['kingdom_title'] = _('[KINGDOM]', lang)
        _class['traits_title'] = _('[TRAITS]', lang)
        _class['traits'] = self.enrich_traits(_class['traits'], lang)
        _class['weapon_title'] = _('[WEAPON]', lang)
        _class['talents'] = translated_trees
        _class['trees'] = [_(f'[TALENT_TREE_{tree.upper()}]', lang) for tree in _class['trees']]
        _class['type_short'] = _(f'[TROOPTYPE_{_class["type"].upper()}]', lang)
        _class['type'] = _(f'[PERK_TYPE_{_class["type"].upper()}]', lang)
        _class['weapon_bonus'] = _('[MAGIC_BONUS]', lang) + " " + _(
            f'[MAGIC_BONUS_{COLORS.index(_class["weapon_color"])}]', lang)

    def get_all_talents(self, lang):
        result = []
        for input_tree in self.talent_trees.values():
            tree = input_tree.copy()
            self.translate_talent_tree(tree, lang)
            result.append(tree)
        return sorted(result, key=operator.itemgetter('name'))

    def search_talent(self, search_term, lang):
        possible_matches = []
        for tree in self.talent_trees.values():
            translated_name = extract_search_tag(_(tree['name'], lang))
            translated_talents = [_(talent['name'], lang) for talent in tree['talents']]
            talents_search_tags = [extract_search_tag(talent) for talent in translated_talents]
            real_search = extract_search_tag(search_term)
            if real_search == translated_name or real_search in talents_search_tags:
                result = tree.copy()
                self.translate_talent_tree(result, lang)
                return [result]
            elif real_search in translated_name:
                result = tree.copy()
                self.translate_talent_tree(result, lang)
                possible_matches.append(result)
            elif talent_matches := [
                tag for tag in talents_search_tags if real_search in tag
            ]:
                result = tree.copy()
                result['talent_matches'] = talent_matches
                self.translate_talent_tree(result, lang)
                possible_matches.append(result)
        return sorted(possible_matches, key=operator.itemgetter('name'))

    @staticmethod
    def translate_talent_tree(tree, lang):
        tree['talents_title'] = _('[TALENT_TREES]', lang)
        tree['name'] = _(tree['name'], lang)
        translated_talents = [
            {
                'name': _(talent['name'], lang),
                'description': _(talent['description'], lang),
            }
            for talent in tree['talents']
        ]
        tree['talents'] = translated_talents
        tree['classes'] = [
            {'id': c['id'],
             'name': _(c['name'], lang)
             }
            for c in tree['classes']
        ]

    def get_troops_with_trait(self, trait, lang):
        return self.get_objects_by_trait(trait, self.troops, self.translate_troop, lang)

    def get_classes_with_trait(self, trait, lang):
        return self.get_objects_by_trait(trait, self.classes, self.translate_class, lang)

    @staticmethod
    def get_objects_by_trait(trait, objects, translator, lang):
        result = []
        for o in objects.values():
            trait_codes = [trait['code'] for trait in o['traits']] if 'traits' in o else []
            if trait['code'] in trait_codes:
                translated_object = o.copy()
                translator(translated_object, lang)
                result.append(translated_object)
        return result

    def search_trait(self, search_term, lang):
        possible_matches = []
        for code, trait in self.traits.items():
            translated_name = extract_search_tag(_(trait['name'], lang))
            translated_description = extract_search_tag(_(trait['description'], lang))
            real_search = extract_search_tag(search_term)
            if real_search == translated_name:
                result = trait.copy()
                result['troops'] = self.get_troops_with_trait(trait, lang)
                result['troops_title'] = _('[TROOPS]', lang)
                result['classes'] = self.get_classes_with_trait(trait, lang)
                result['classes_title'] = _('[CLASS]', lang)
                if result['troops'] or result['classes']:
                    return self.enrich_traits([result], lang)
            elif real_search in translated_name or real_search in translated_description:
                result = trait.copy()
                result['troops'] = self.get_troops_with_trait(trait, lang)
                result['troops_title'] = _('[TROOPS]', lang)
                result['classes'] = self.get_classes_with_trait(trait, lang)
                result['classes_title'] = _('[CLASS]', lang)
                if result['troops'] or result['classes']:
                    possible_matches.append(result)
        return sorted(self.enrich_traits(possible_matches, lang), key=operator.itemgetter('name'))

    def search_pet(self, search_term, lang):
        return self.pets.search(search_term, lang)

    def search_weapon(self, search_term, lang):
        lookup_keys = [
            'name',
            'type',
            'roles',
            'spell.description',
        ]
        return self.search_item(search_term, lang,
                                items=self.weapons,
                                lookup_keys=lookup_keys,
                                translator=self.translate_weapon)

    def translate_weapon(self, weapon, lang):
        weapon['name'] = _(weapon['name'], lang)
        weapon['description'] = _(weapon['description'], lang)
        weapon['color_code'] = "".join(sorted(weapon['colors']))
        weapon['spell_title'] = _('[TROOPHELP_SPELL0]', lang)
        weapon['rarity_title'] = _('[RARITY]', lang)
        weapon['raw_rarity'] = weapon['rarity']

        rarity_number = WEAPON_RARITIES.index(weapon['rarity'])
        weapon['rarity'] = _(f'[RARITY_{rarity_number}]', lang)
        weapon['spell'] = self.translate_spell(weapon['spell_id'], lang)
        weapon['upgrade_title'] = _('[UPGRADE_WEAPON]', lang)

        bonus_title = _('[BONUS]', lang)
        upgrade_numbers = zip(weapon['armor_increase'], weapon['attack_increase'], weapon['health_increase'],
                              weapon['magic_increase'])
        upgrade_titles = (
            _('[ARMOR]', lang),
            _('[ATTACK]', lang),
            _('[LIFE]', lang),
            _('[MAGIC]', lang),
        )
        upgrades = []
        for upgrade in upgrade_numbers:
            upgrades.extend(
                {
                    'name': f'{upgrade_titles[i]} {bonus_title}',
                    'description': f'+{amount} {upgrade_titles[i]}',
                }
                for i, amount in enumerate(upgrade)
                if amount
            )
        weapon['upgrades'] = upgrades + [self.translate_spell(spell['id'], lang) for spell in weapon['affixes']]
        weapon['kingdom_title'] = _('[KINGDOM]', lang)
        weapon['kingdom_id'] = weapon['kingdom']['id']
        weapon['kingdom'] = _(weapon['kingdom']['name'], lang, default=weapon['kingdom']['reference_name'])
        weapon['roles_title'] = _('[WEAPON_ROLE]', lang)
        weapon['roles'] = [_(f'[TROOP_ROLE_{role.upper()}]', lang) for role in weapon['roles']]
        weapon['type_title'] = _('[FILTER_WEAPONTYPE]', lang)
        weapon['type'] = _(f'[WEAPONTYPE_{weapon["type"].upper()}]', lang)

        weapon['has_mastery_requirement_color'] = False
        if weapon['requirement'] < 1000:
            weapon['requirement_text'] = _('[WEAPON_MASTERY_REQUIRED]', lang) + \
                                         str(weapon['requirement'])
            weapon['has_mastery_requirement_color'] = True
        elif weapon['requirement'] == 1000:
            weapon['requirement_text'] = _('[WEAPON_AVAILABLE_FROM_CHESTS_AND_EVENTS]', lang)
        elif weapon['requirement'] == 1002:
            _class = _(weapon.get('class', '[NO_CLASS]'), lang)
            weapon['requirement_text'] = _('[CLASS_REWARD_TITLE]', lang) + f' ({_class})'
        elif weapon['requirement'] == 1003:
            weapon['requirement_text'] = _('[SOULFORGE_WEAPONS_TAB_EMPTY_ERROR]', lang)
        if weapon.get('event_faction'):
            weapon['requirement_text'] += ' (' + _(f'[{weapon["event_faction"]}_NAME]', lang) + ' ' + _(
                '[FACTION_WEAPON]', lang) + ')'

    def search_affix(self, search_term, lang):
        real_search = extract_search_tag(search_term)
        results = {}
        for weapon in self.weapons.values():
            my_weapon = weapon.copy()
            self.translate_weapon(my_weapon, lang)
            affixes = [affix for affix in my_weapon['upgrades'] if 'cost' in affix]
            for affix in affixes:
                search_name = extract_search_tag(affix['name'])
                search_desc = extract_search_tag(affix['description'])
                if real_search == search_name \
                        or real_search == search_desc \
                        or real_search in search_name \
                        or real_search in search_desc:
                    if affix['name'] in results:
                        results[affix['name']]['weapons'].append(my_weapon)
                        results[affix['name']]['num_weapons'] += 1
                    else:
                        results[affix['name']] = affix.copy()
                        results[affix['name']]['weapons_title'] = _('[SOULFORGE_TAB_WEAPONS]', lang)
                        results[affix['name']]['weapons'] = [my_weapon]
                        results[affix['name']]['num_weapons'] = 1
        for name, affix in results.items():
            if real_search == extract_search_tag(name):
                return [affix]
        return sorted(results.values(), key=operator.itemgetter('name'))

    def search_traitstone(self, search_term, lang):
        return self.search_item(search_term, lang,
                                items=self.traitstones,
                                lookup_keys=['name'],
                                translator=self.translate_traitstone)

    def translate_traitstone(self, traitstone, lang):
        troops = []
        for troop_id in traitstone['troop_ids']:
            amount = sum(troop['amount']
                         for troop in self.troops[troop_id]['traitstones']
                         if troop['id'] == traitstone['id'])
            troops.append([_(self.troops[troop_id]['name'], lang), amount])
        traitstone['troops'] = sorted(troops, key=operator.itemgetter(1), reverse=True)

        classes = []
        for class_id in traitstone['class_ids']:
            amount = sum(_class['amount'] for _class in self.classes[class_id]['traitstones']
                         if _class['id'] == traitstone['id'])
            classes.append([_(self.classes[class_id]['name'], lang), amount])
        traitstone['classes'] = classes

        kingdoms = [
            _(self.kingdoms[int(kingdom_id)]['name'], lang)
            for kingdom_id in traitstone['kingdom_ids']
        ]
        if not traitstone['kingdom_ids']:
            kingdoms.append(_('[ALL_KINGDOMS]', lang))
        traitstone['kingdoms'] = kingdoms

        traitstone['name'] = _(traitstone['name'], lang)
        traitstone['troops_title'] = _('[TROOPS]', lang)
        traitstone['classes_title'] = _('[CLASS]', lang)
        traitstone['kingdoms_title'] = _('[KINGDOMS]', lang)

    def translate_spell(self, spell_id, lang):
        spell = self.spells[spell_id]
        magic = _('[MAGIC]', lang)

        description = self.translate_spell_description(spell['description'], lang)

        for i, (multiplier, amount) in enumerate(spell['effects'], start=1):
            spell_amount = f' + {amount}' if amount else ''
            divisor, multiplier_text = self.translate_spell_multiplier(multiplier)
            damage = f'[{multiplier_text}{magic}{divisor}{spell_amount}]'
            number_of_replacements = len(re.findall(r'\{\d}', description))
            has_half_replacement = len(spell['effects']) == number_of_replacements - 1
            if '{2}' in description and has_half_replacement:
                multiplier *= 0.5
                amount *= 0.5
                if amount == int(amount):
                    amount = int(amount)
                half_damage = f'[{multiplier} ⨯ {magic}{divisor} + {amount}]'
                description = description.replace('{1}', half_damage)
                description = description.replace('{2}', damage)
            else:
                description = description.replace(f'{{{i}}}', damage)

        boost = self.calculate_boost(spell)

        description = f'{description}{boost}'

        return {
            'name': _(spell['name'], lang),
            'cost': spell['cost'],
            'description': description,
        }

    def translate_spell_multiplier(self, multiplier):
        multiplier_text = ''
        if multiplier > 1:
            if multiplier == int(multiplier):
                multiplier_text = f'{multiplier:.0f} ⨯ '
            else:
                multiplier_text = f'{multiplier} ⨯ '
        divisor = ''
        if multiplier < 1:
            number = int(round(1 / multiplier))
            divisor = f' / {number}'
        return divisor, multiplier_text

    @staticmethod
    def calculate_boost(spell):
        boost = ''
        if spell['boost']:
            if spell['boost'] > 100:
                boost = f' [x{int(round(spell["boost"] / 100))}]'
            elif spell['boost'] != 1:
                boost = f' [{100 / spell["boost"]:0.0f}:1]'
        return boost

    def translate_spell_description(self, description, lang):
        description = _(description, lang)
        if description.startswith('&&'):
            description = description \
                .replace('&&', _('[CHOICE_CHOOSE_ONE_DESC]', lang), 1) \
                .replace('&&', _('[OR_CAPITALISED]', lang))
        return description

    def translate_banner(self, banner, lang):
        result = {
            'name': _(banner['name'], lang),
            'kingdom': _(self.kingdoms[banner['id']]['name'], lang),
            'colors': [(_(c[0], 'en').lower(), c[1]) for c in banner['colors'] if c[1]],
            'filename': banner['filename'],
        }
        colors_shorthand = []
        for color, amount in result['colors']:
            if amount > 0:
                colors_shorthand.append(color[0].upper())
            else:
                colors_shorthand.append(color[0].lower())
        result['colors_shorthand'] = ''.join(colors_shorthand)
        if not result['colors']:
            result['available'] = _('[AVAILABLE_FROM_KINGDOM]', lang).replace('%1', _(f'[{banner["id"]}_NAME]', lang))
        return result

    def get_event_kingdoms(self, lang):
        today = datetime.date.today()
        start = today + datetime.timedelta(days=-today.weekday(), weeks=1)
        result = self.guess_weekly_kingdom_from_troop_spoilers(lang)

        prediction = ''
        for kingdom_id in self.event_kingdoms:
            end = start + datetime.timedelta(days=7)
            if kingdom_id != 0:
                event_data = {
                    'start': start,
                    'end': end,
                    'kingdom': _(self.kingdoms[kingdom_id]['name'], lang,
                                 default=self.kingdoms[kingdom_id]['reference_name']) + prediction,
                }
                result[start] = event_data
            else:
                prediction = ' *'
            start = end
        return sorted(result.values(), key=operator.itemgetter('start'))

    def guess_weekly_kingdom_from_troop_spoilers(self, lang):
        result = {}
        latest_date = datetime.datetime.utcnow().replace(tzinfo=datetime.timezone.utc)
        for spoiler in self.spoilers:
            if spoiler['type'] == 'troop' \
                    and spoiler['date'].weekday() == 0 \
                    and spoiler['date'] > latest_date:
                troop = self.troops[spoiler['id']]
                if troop['rarity'] == 'Mythic':
                    continue
                kingdom = troop['kingdom']
                if not kingdom.get('name') and not kingdom.get('reference_name'):
                    continue
                if kingdom['id'] in self.event_kingdoms:
                    continue
                result[spoiler['date'].date()] = {
                    'start': spoiler['date'].date(),
                    'end': spoiler['date'].date() + datetime.timedelta(days=7),
                    'kingdom': _(kingdom['name'], lang,
                                 default=kingdom['reference_name']) + ' *',
                }
                latest_date = spoiler['date']
        return result

    def get_events(self, lang):
        today = datetime.date.today()
        return [
            self.translate_event(e, lang)
            for e in self.events
            if today <= e['start']
        ]

    def _extend_event_extra_info(self, entry, lang):
        event_type = entry['type']
        gacha = entry['gacha']
        gacha_pools = {
            '[BOUNTY]': self.troops,
            '[HIJACK]': self.troops,
            '[DELVE_EVENT]': self.troops,
            '[RAIDBOSS]': self.troops,
            '[TOWER_OF_DOOM]': self.troops,
            '[CLASS_EVENT]': self.classes,
        }
        gacha_pool = gacha_pools.get(event_type)
        if gacha_pool and gacha and gacha in gacha_pool:
            item = gacha_pool[gacha]
            return _(item['name'], lang, default=item.get('reference_name', item['name']))
        elif event_type == '[PETRESCUE]' and gacha and gacha in self.pets:
            return self.pets[gacha][lang].name
        elif event_type == '[HIJACK]' and entry['troops']:
            troops = [self.troops[troop] for troop in entry['troops']]
            return ', '.join(_(troop['name'], lang, default=troop['reference_name']) for troop in troops)
        return ''

    def translate_event(self, event, lang):
        entry = event.copy()

        entry['extra_info'] = self._extend_event_extra_info(entry, lang)
        if entry['type'] == '[INVASION]' and entry['gacha'] and entry['gacha'] in self.troops:
            troop = self.troops[entry['gacha']]
            troop_name = _(troop['name'], lang)
            entry['kingdom_id'] = troop.get('kingdom_id', '`?`')
            troop_types = [_(f'[TROOPTYPE_{tt.upper()}]', lang) for tt in troop['types']]
            entry['extra_info'] = f'{troop_name} ({", ".join(troop_types)})'
        elif entry['type'] in ('[WEEKLY_EVENT]', '[RARITY_5]') and entry['gacha'] and entry['gacha'] in self.troops:
            troop = self.troops[entry['gacha']]
            troop_name = _(troop['name'], lang, default=troop['reference_name'])
            kingdom = _(self.kingdoms[entry['kingdom_id']]['name'], lang,
                        default=self.kingdoms[entry['kingdom_id']]['reference_name'])
            entry['extra_info'] = f'{troop_name} ({kingdom})'
            entry['kingdom'] = kingdom
        elif entry['type'] == '[VAULT]':
            entry['kingdom_id'] = 3038

        if entry['kingdom_id']:
            kingdom = self.kingdoms[entry['kingdom_id']]
            entry['kingdom'] = _(kingdom['name'], lang, default=kingdom['reference_name'])

        locale = translations.LANGUAGE_CODE_MAPPING.get(lang, lang)
        locale = translations.LOCALE_MAPPING.get(locale, 'en_GB') + '.UTF8'
        with calendar.different_locale(locale):
            entry['formatted_start'] = entry['start'].strftime(WEEK_DAY_FORMAT)
            entry['start_day'] = entry['start'].strftime('%A')
            entry['formatted_end'] = entry['end'].strftime(WEEK_DAY_FORMAT)
            entry['end_day'] = entry['end'].strftime('%A')

        entry['raw_type'] = entry['type']
        entry['type'] = _(entry['type'], lang)
        if self.is_untranslated(entry['type']) and entry['names']:
            entry['type'] = entry['names'][translations.LOCALE_MAPPING[lang].replace('en_GB', 'en_US')]

        return entry

    def get_campaign_tasks(self, lang, _filter=None):
        result = {'heading': f'{_("[CAMPAIGN]", lang)}: {_("[TASKS]", lang)}'}
        tiers = ['bronze', 'silver', 'gold']
        result['campaigns'] = {
            f'[MEDAL_LEVEL_{i}]': [self.translate_campaign_task(task, lang) for task in self.campaign_tasks[tier]]
            for i, tier in reversed(list(enumerate(tiers))) if _filter is None or tier.lower() == _filter.lower()
        }
        formatted_start, start_date = get_next_monday_in_locale(date=None, lang=lang)
        result['has_content'] = any(len(c) > 0 for c in result['campaigns'].values())
        result['background'] = f'Background/{self.campaign_tasks["kingdom"]["filename"]}_full.png'
        result['gow_logo'] = 'Atlas/gow_logo.png'
        kingdom_filebase = self.campaign_tasks['kingdom']['filename']
        result['kingdom_logo'] = f'Troopcardshields_{kingdom_filebase}_full.png'
        result['kingdom'] = _(self.campaign_tasks['kingdom']['name'], lang)
        result['raw_date'] = start_date
        result['date'] = formatted_start
        result['lang'] = lang
        result['texts'] = {
            'campaign': _('[CAMPAIGN]', lang),
            'team': _('[LITE_CHAT_TEAM_START]', lang),
        }
        return result

    def get_reroll_tasks(self, lang, _filter=None):
        tiers = ['bronze', 'silver', 'gold']
        return {
            f'[MEDAL_LEVEL_{i}]': [
                self.translate_campaign_task(task, lang)
                for task in self.reroll_tasks[tier]
            ]
            for i, tier in reversed(list(enumerate(tiers)))
            if _filter is None or tier.lower() == _filter.lower()
        }

    def __task_name_replacements(self, task, color, lang):
        replacements = {
            '{WeaponType}': '[WEAPONTYPE_{c:u}]',
            '{Kingdom}': '[{d:u}_NAME]',
            '{Banner}': '[{c:u}_BANNERNAME]',
            '{Class}': '[HEROCLASS_{c:l}_NAME]',
            '{Color}': f'[GEM_{color}]',
            '{TroopType}': '[TROOPTYPE_{value1:u}]',
            '{Troop}': '{{[{value1}][name]}}',
            '{Value0}': task['value0'],
            '{Value1}': task['value1'],
            '{0}': '{x}',
            '{1}': task['c'],
            '{2}': '{x} {y}',
        }
        for before, after in replacements.items():
            if before in task['title'] or before in task['name']:
                translated = _(after.format(**task).format(self.troops), lang, plural=task['plural'])
                if '`?`' in translated:
                    translated = '`?`'
                task['title'] = task['title'].replace(before, translated)
                task['name'] = task['name'].replace(before, translated)

    def translate_campaign_task(self, task, lang):
        new_task = task.copy()
        color_code = int(new_task['value1']) if new_task['value1'].isdigit() else 666
        color = COLORS[color_code].upper() if color_code < len(COLORS) else '`?`'
        if isinstance(new_task.get('y'), str):
            new_task['y'] = _(f'[{new_task["y"].upper()}]', lang)
        new_task['plural'] = int(new_task.get('x', 1)) != 1
        new_task['title'] = _(new_task['title'], lang, plural=new_task['plural'])
        new_task['name'] = _(new_task["name"], lang, plural=new_task['plural'])
        if '{0}' not in new_task['name'] and '{2}' not in new_task['name']:
            new_task['name'] = f'{task["x"]}x ' + new_task['name']

        self.__task_name_replacements(new_task, color, lang)
        new_task['name'] += self.__task_solution_location(new_task, task, color, lang)

        return new_task

    def __task_solution_location(self, new_task, task, color, lang):
        where = ''
        if new_task['value1'] == '`?`':
            pass
        elif task['name'] == '[TASK_KILL_TROOP_COLOR]' and color != '`?`':
            color_kingdoms = self.get_color_kingdoms(lang)
            target_kingdom = color_kingdoms[color.lower()]['name']
            where = f' --> {target_kingdom}'
        elif task['name'] == '[TASK_KILL_TROOP_ID]':
            target_kingdom = _(self.troops[int(task['value1'])]['kingdom']['name'], lang)
            pvp = _('[PVP]', lang)
            weekly_event = _('[WEEKLY_EVENT]', lang)
            where = f' --> {target_kingdom} / {pvp} / {weekly_event}'
        elif task['name'] == '[TASK_KILL_TROOP_TYPE]':
            troop_type_kingdoms = dict(self.get_type_kingdoms(lang))
            troop_type = _(f'[TROOPTYPE_{task["value1"].upper()}]', lang)
            target_kingdom = troop_type_kingdoms[troop_type]['name']
            where = f' --> {target_kingdom}'
        elif task['name'] == '[TASK_KILL_TREASURE_GNOMES]':
            vault = _(self.kingdoms[3038]['name'], lang)
            where = f' --> {vault}'
        return where

    def get_spoilers(self, lang):
        spoilers = []
        now = datetime.datetime.utcnow().replace(tzinfo=datetime.timezone.utc)
        near_term_spoilers = [s for s in self.spoilers if now <= s['date'] <= now + datetime.timedelta(days=180)]
        for spoiler in near_term_spoilers:
            if translated := self.translate_spoiler(spoiler, lang):
                spoilers.append(translated)
        return spoilers

    def translate_spoiler(self, spoiler, lang):
        # this is transitional until all new models are in place.
        if spoiler['type'] in ['pet']:
            if item := getattr(self, spoiler['type'] + 's').get(spoiler['id']):
                entry = item[translations.LANGUAGE_CODE_MAPPING.get(lang, lang)].data.copy()
            else:
                return
        else:
            entry = getattr(self, spoiler['type'] + 's').get(spoiler['id'], {}).copy()
        if not entry:
            return None
        entry['name'] = _(entry['name'], lang)
        if self.is_untranslated(entry['name']):
            entry['name'] = entry.get('reference_name', entry['name'])
        entry['type'] = spoiler['type']
        entry['date'] = spoiler['date'].date()
        entry['event'] = _('[GLOG_EVENT]', lang) + ': ' if entry.get('event') else ''
        if 'rarity' in entry:
            entry['rarity_title'] = _('[RARITY]', lang)
            if entry['rarity'] in TROOP_RARITIES:
                rarity_number = TROOP_RARITIES.index(entry['rarity'])
                entry['rarity'] = _(f'[RARITY_{rarity_number}]', lang)

        if kingdom_id := entry.get('kingdom_id'):
            kingdom = self.kingdoms[kingdom_id]
            entry['kingdom'] = _(kingdom['name'], lang)
            if self.is_untranslated(entry['kingdom']):
                entry['kingdom'] = kingdom['reference_name']
        return entry

    def get_soulforge(self, lang):
        title = _('[SOULFORGE]', lang)
        craftable_items = {}
        for category, recipes in self.soulforge.items():
            recipe_type = _(category, lang)
            craftable_items[recipe_type] = [self.translate_recipe(r, lang) for r in recipes]
        return title, craftable_items

    def get_summons(self, lang):
        title = _('[SUMMONING_STONE_MENU_HEADING]', lang)
        result = {}
        for stone, contents in self.summons.items():
            stone_name = _(stone, lang)
            troops = [
                {
                    'name': _(self.troops[troop['troop_id']]['name'], lang),
                    'rarity': self.troops[troop['troop_id']]['rarity'],
                    'count': troop['count'],
                    'id': troop['troop_id']
                }
                for troop in contents
            ]
            result[stone_name] = troops
        return title, result

    @staticmethod
    def translate_recipe(recipe, lang):
        new_recipe = recipe.copy()
        new_recipe['name'] = _(recipe['name'], lang)
        rarity_number = WEAPON_RARITIES.index(new_recipe['rarity'])
        new_recipe['rarity_number'] = rarity_number
        new_recipe['raw_rarity'] = new_recipe['rarity']
        new_recipe['rarity'] = _(f'[RARITY_{rarity_number}]', lang)
        return new_recipe

    @staticmethod
    def translate_categories(categories, lang):
        def try_different_translated_versions_because_devs_are_stupid(cat):
            lookup = f'[{cat.upper()}S]'
            result = _(lookup, lang)
            if result == lookup:
                lookup = f'[{cat.upper()}S:]'
                result = _(lookup, lang)[:-1]
            if result == lookup[:-1]:
                result = _(f'[{cat.upper()}]', lang)
            if result == '[CLASSE]':
                result = _('[CLASS]', lang)
            return result

        translated = [try_different_translated_versions_because_devs_are_stupid(c) for c in categories]
        return dict(zip(categories, translated))

    def get_levels(self, lang):
        return [
            {
                'level': level['level'],
                'bonus': _(level['bonus'], lang),
            }
            for level in self.levels
        ]

    def translate_toplist(self, toplist_id, lang):
        toplist = self.toplists.get(toplist_id)
        if not toplist:
            return None
        result = toplist.copy()
        result['items'] = []
        for item_search in toplist['items']:
            items = self.search_troop(item_search, lang)
            if not items:
                items = self.search_weapon(item_search, lang)
            if not items:
                continue
            result['items'].append(items[0])
        return result

    async def create_toplist(self, message, description, items, lang, update_id):
        toplist_id = await self.toplists.add(message.author.id, message.author.display_name, description, items,
                                             update_id)
        return self.translate_toplist(toplist_id, lang)

    def kingdom_percentage(self, filter_name, filter_values, lang):
        result = {}
        now = datetime.datetime.utcnow().replace(tzinfo=datetime.timezone.utc)
        hidden_kingdoms = [3032, 3033, 3034, 3038]

        for filter_ in filter_values:
            kingdoms = []
            for kingdom in self.kingdoms.values():
                if kingdom['location'] != 'krystara':
                    continue
                if kingdom['id'] in hidden_kingdoms:
                    continue
                all_troops = [self.troops.get(troop_id) for troop_id in kingdom['troop_ids']]
                explore_troops = [
                    troop for troop in all_troops
                    if troop
                       and troop.get('release_date', now) <= now
                       and troop.get('rarity') not in [None, 'Legendary', 'Mythic']
                ]
                if not explore_troops:
                    continue
                fitting_troops = [troop for troop in explore_troops if filter_ in troop[filter_name]]
                kingdoms.append({
                    'name': _(kingdom['name'], lang),
                    'total': len(explore_troops),
                    'fitting_troops': len(fitting_troops),
                    'percentage': len(fitting_troops) / len(explore_troops),
                })
            top_kingdom = sorted(kingdoms, key=operator.itemgetter('percentage'), reverse=True)[0]
            result[filter_] = top_kingdom
        return result

    def get_color_kingdoms(self, lang):
        colors_without_skulls = COLORS[:6]
        return self.kingdom_percentage('colors', colors_without_skulls, lang)

    def get_type_kingdoms(self, lang):
        forbidden_types = {'None', 'Boss', 'Tower', 'Castle', 'Doom', 'Gnome'}
        troop_types = self.troop_types - forbidden_types
        result = self.kingdom_percentage('types', troop_types, lang)
        translated_result = {
            _(f"[TROOPTYPE_{troop_type.upper()}]", lang): kingdom
            for troop_type, kingdom in result.items()
        }
        return sorted(translated_result.items(), key=operator.itemgetter(0))

    def get_adventure_board(self, lang):
        return [
            self.translate_adventure(adventure, lang)
            for adventure in self.adventure_board
        ]

    @staticmethod
    def translate_adventure(adventure, lang):
        def change_form(key, value):
            if value == 1 and key.startswith('[KEYTYPE'):
                key = key.replace('_TITLE', '_SINGLE')
            return _(key, lang).replace('%1 ', ''), value

        result = adventure.copy()
        result['name'] = _(result['name'], lang)
        result['reward_types'] = set(result['rewards'].keys())
        result['rewards'] = dict([change_form(key, value) for key, value in result['rewards'].items()])
        result['rarity'] = _(result['rarity'], lang)
        return result

    @staticmethod
    def is_untranslated(param):
        return param[0] + param[-1] == '[]' if param else True

    def get_toplist_troop_ids(self, items, lang):
        result = []
        for search_term in items.split(','):
            items = self.search_troop(search_term, lang)
            if not items:
                items = self.search_weapon(search_term, lang)
            if items:
                result.append(str(items[0]['id']))
        return result

    def get_soulforge_weapon_image_data(self, search_term, date, switch, lang):
        search_result = self.search_weapon(search_term, lang)
        if len(search_result) != 1:
            return
        weapon = search_result[0].copy()

        requirements = SOULFORGE_REQUIREMENTS[weapon['raw_rarity']].copy()
        alternate_kingdom_id = weapon.get('event_faction')
        if alternate_kingdom_id:
            requirements = UNDERWORLD_SOULFORGE_REQUIREMENTS[weapon['raw_rarity']].copy()

        jewels = []
        for color in weapon['colors']:
            color_code = COLORS.index(color)
            filename = f'Runes_Jewel{color_code:02n}_full.png'
            jewels.append({
                'filename': filename,
                'amount': requirements['jewels'],
                'available_on': translate_day(color_code, lang),
                'kingdoms': sorted([_(kingdom['name'], lang) for kingdom in self.kingdoms.values()
                                    if 'primary_color' in kingdom
                                    and color == kingdom['primary_color']
                                    and kingdom['location'] == 'krystara']),
            })
        requirements['jewels'] = jewels
        kingdom = self.kingdoms[weapon['kingdom_id']]
        alternate_kingdom_name = None
        alternate_kingdom_filename = None
        if alternate_kingdom_id:
            alternate_kingdom = self.kingdoms[alternate_kingdom_id]
            alternate_kingdom_name = _(alternate_kingdom['name'], lang)
            alternate_kingdom_filename = alternate_kingdom['filename']

        affixes = [{
            'name': _(affix['name'], lang),
            'description': _(affix['description'], lang),
            'color': list(RARITY_COLORS.values())[i],
        } for i, affix in enumerate(weapon['affixes'], start=1)]
        mana_colors = ''.join([c.title() for c in weapon['colors']]).replace('Brown', 'Orange')
        kingdom_filebase = self.kingdoms[weapon['kingdom_id']]['filename']
        in_soulforge_text = _('[WEAPON_AVAILABLE_FROM_SOULFORGE]', lang)
        if alternate_kingdom_id:
            in_soulforge_text += ' (' + _(f'[{weapon["event_faction"]}_NAME]', lang) + ' ' + _(
                '[FACTION_WEAPON]', lang) + ')'
        date = get_next_monday_in_locale(date, lang)[0]
        return {
            'switch': switch,
            'name': weapon['name'],
            'rarity_color': RARITY_COLORS[weapon['raw_rarity']],
            'rarity': weapon['rarity'],
            'filename': f'Spells/Cards_{weapon["spell_id"]}_full.png',
            'description': weapon['spell']['description'],
            'kingdom': weapon['kingdom'],
            'alternate_kingdom': alternate_kingdom_name,
            'kingdom_logo': f'Troopcardshields_{kingdom_filebase}_full.png',
            'alternate_kingdom_logo': f'Troopcardshields_{alternate_kingdom_filename}_full.png',
            'type': _(weapon['type'], lang),
            'background': f'Background/{kingdom["filename"]}_full.png',
            'gow_logo': 'Atlas/gow_logo.png',
            'requirements': requirements,
            'affixes': affixes,
            'affix_icon': 'Atlas/affix.png',
            'gold_medal': 'Atlas/medal_gold.png',
            'mana_color': f'Troopcardall_{mana_colors}_full.png',
            'mana_cost': weapon['spell']['cost'],
            'stat_increases': {
                'attack': sum(weapon['attack_increase']),
                'health': sum(weapon['health_increase']),
                'armor': sum(weapon['armor_increase']),
                'magic': sum(weapon['magic_increase']),
            },
            'stat_icon': 'Atlas/{stat}.png',
            'texts': {
                'from_battles': _('[PET_LOOT_BONUS]', lang)
                .replace('+%1% %2 ', '')
                .replace('+%1 %2 ', ''),
                'gem_bounty': _('[DUNGEON_OFFER_NAME]', lang),
                'kingdom_challenges': f'{_("[KINGDOM]", lang)} {_("[CHALLENGES]", lang)}',
                'soulforge': _('[SOULFORGE]', lang),
                'resources': _('[RESOURCES]', lang),
                'dungeon': _('[DUNGEON]', lang),
                'dungeon_battles': _('[TASK_WIN_DUNGEON_BATTLES]', lang)
                .replace('{0}', '3')
                .replace('\x19', 's'),
                'tier_8': _('[CHALLENGE_TIER_8_ROMAN]', lang),
                'available': _('[AVAILABLE]', lang),
                'in_soulforge': in_soulforge_text,
                'n_gems': _('[GEMS_GAINED]', lang).replace('%1', '50'),
            },
            'date': date,
        }

    def translate_drop_chances(self, data: dict, lang):
        for key, item in data.copy().items():
            if not self.is_untranslated(key):
                continue
            new_key = _(key, lang)
            if key == '[KEYTYPE_5_TITLE]':
                new_key = f'{new_key}*'
            data[new_key] = item.copy()
            if key != new_key:
                del data[key]
            if isinstance(data[new_key], dict):
                self.translate_drop_chances(data[new_key], lang)

    def get_drop_chances(self, lang):
        drop_chances = self.drop_chances.copy()
        self.translate_drop_chances(drop_chances, lang)
        return drop_chances

    @staticmethod
    def get_shop_rewards(event, lang, emojis):
        event['shop_title'] = _('[SHOP]', lang)
        event['shop'] = []
        total_cost = 0
        total = _('[TOTAL]', lang)
        for shop_tier in event['shop_tiers']:
            if rewards := [
                _(r['name'], lang).replace('%1', str(r['amount']))
                for r in shop_tier['rewards']
            ]:
                total_cost += shop_tier['cost']
                currency = _(shop_tier['currency'], lang)
                if shop_tier['currency'] == '[GEMS]':
                    currency = emojis.get('gems')
                shop_display = f'**{_(shop_tier["title"], lang)}** ({shop_tier["cost"]} ' \
                               f'{currency}, {total} {total_cost}): ' \
                               f'{", ".join(rewards)}'
                event['shop'].append(shop_display)

    def get_event_rewards(self, event, lang):
        for stage, stage_reward in event['rewards'].items():
            stage_reward['name'] = _('[REWARD_N]', lang).replace('%1', str(stage))
            if EVENT_TYPES[event['type']] == '[RAIDBOSS]':
                if stage <= 2:
                    stage_reward['name'] = _('[MINIONS_N]').replace('%1', str(stage))
                else:
                    stage_reward['name'] = _('[PORTAL_N]', lang).replace('%1', str(stage - 2))

            for reward in stage_reward['rewards']:
                reward_type = reward['type']
                reward['type'] = _(reward_type, lang).replace('%1', '').strip()
                if reward_type == '[TITLE]':
                    reward['type'] += ' (' + _(f'[TITLE_{reward["data"]}]', lang) + ')'
                if reward_type == '[TROOP]':
                    reward['type'] = _(self.troops.get(reward['data'])['name'], lang)

    @staticmethod
    def get_event_medals(event, lang):
        for item in ('token', 'badge', 'medal'):
            if not event[item]:
                continue
            event[item] = {
                'name': _(f'[WONDER_{event[item]}_NAME]', lang),
                'description': _(f'[WONDER_{event[item]}_DESC]', lang),
            }

    def get_current_event(self, lang, emojis):
        event = copy.deepcopy(self.weekly_event)
        kingdoms = self.search_kingdom(event['kingdom_id'], lang)
        if kingdoms:
            event['kingdom'] = kingdoms[0]
        event['name'] = event['name'].get(lang, _(EVENT_TYPES[event['type']], lang))
        event['lore'] = event['lore'].get(lang, '')
        event['currencies'] = [{
            'name': currency['name'].get(lang, ''),
            'icon': currency['icon'],
            'value': _('[N_TIMES_POINTS]', lang).replace('%1', str(currency['value']))
        } for currency in event['currencies']]

        self.get_shop_rewards(event, lang, emojis)
        self.get_event_rewards(event, lang)
        self.get_event_medals(event, lang)

        def translate_restriction(title, restriction):
            if title == '[FILTER_MANACOLOR]':
                return emojis.get(COLORS[restriction])
            elif title == '[FILTER_ROLE]':
                return _(restriction, lang)
            elif title == '[KINGDOM]':
                return _(restriction, lang)
            elif title == '[TROOP_TYPES]':
                return _(f'[TROOPTYPE_{restriction.upper()}]', lang)
            """
            unknown, but possible restrictions:
                [FILTER_WEAPONTYPE]
                [RARITY]
                [ROSTER]
            """

        def translate_restrictions(title, restrictions):
            return [translated
                    for r in restrictions
                    if (translated := translate_restriction(title, r))]

        def translate_battle(b):
            result = b.copy()
            result['name'] = b['names'].get(lang)
            result['troops'] = []
            del result['names']
            for troop_id in b['ids']:
                troop = self.troops.get(troop_id).copy()
                self.translate_troop(troop, lang)
                result['troops'].append(troop)
            return result

        troop_restriction_types = (
            '[FILTER_MANACOLOR]', '[FILTER_ROLE]', '[KINGDOM]', '[RARITY]', '[ROSTER]', '[TROOP_TYPES]')
        event['troop_restrictions'] = {_(r, lang): ', '.join(translate_restrictions(r, v)) for r, v in
                                       event['restrictions'].items() if v and r in troop_restriction_types}
        weapon_restriction_types = ('[FILTER_MANACOLOR]', '[FILTER_WEAPONTYPE]', '[KINGDOM]')
        if EVENT_TYPES[event['type']] != '[TOWER_OF_DOOM]':
            event['weapon_restrictions'] = {_(r, lang): ', '.join(translate_restrictions(r, v)) for r, v in
                                            event['restrictions'].items() if v and r in weapon_restriction_types}
        event['troop'] = _(event['troop'], lang)
        if event['weapon_id']:
            event['weapon'] = _(self.weapons.get(event['weapon_id'], {'name': ''})['name'], lang)

        event['battles_title'] = _('[BATTLES]', lang)
        new_battles = []
        for battle in event['battles']:
            tb = translate_battle(battle)
            if tb['name'] and tb not in new_battles:
                new_battles.append(tb)
        event['battles'] = sorted(new_battles, key=operator.itemgetter('raw_rarity'), reverse=True)

        locale = translations.LANGUAGE_CODE_MAPPING.get(lang, lang)
        locale = translations.LOCALE_MAPPING.get(locale, 'en_GB') + '.UTF8'
        with calendar.different_locale(locale):
            event['formatted_start'] = event['start'].strftime(WEEK_DAY_FORMAT)
            event['formatted_end'] = event['end'].strftime(WEEK_DAY_FORMAT)

        return event

    def get_effects(self, lang):
        positive = _('[TROOPHELP_ALLPOSITIVESTATUSEFFECTS_1]', lang)
        negative = _('[TROOPHELP_ALLNEGATIVESTATUSEFFECTS_1]', lang)
        result = {
            positive: [],
            negative: [],
        }
        for effect in self.effects:
            key = positive if effect in self.positive_effects else negative
            result[key].append({
                'name': _(f'[TROOPHELP_{effect}_1]', lang),
                'description': _(f'[TROOPHELP_{effect}_2]', lang),
            })
        result[positive] = sorted(result[positive], key=operator.itemgetter('name'))
        result[negative] = sorted(result[negative], key=operator.itemgetter('name'))
        return result

    def get_active_gems(self, lang):
        result = []
        for gem in self.active_gems.values():
            active_gem = gem.copy()
            tutorial_key = f'[TUTORIAL_DESCRIPTION_{gem["tutorial"]}]'
            active_gem['tutorial'] = _(tutorial_key, lang)
            result.append(active_gem)
        return result

    @staticmethod
    def get_heroic_gems(lang):
        result = {}
        for gem, tutorial_id in GEM_TUTORIAL_IDS.items():
            if not tutorial_id:
                continue
            tutorial = _(f'[TUTORIAL_DESCRIPTION_{tutorial_id}]', lang)
            result.setdefault(tutorial, []).append(gem)
        return result

    @staticmethod
    def get_storms(lang):
        storms = {}
        fields = {
            '1': 'name',
            '2': 'description',
        }
        p = re.compile(r'\[TROOPHELP_STORM(\d+_?)+')
        for key, value in t.all_translations[lang].items():
            if not p.match(key):
                continue
            field = fields[key[-2]]
            storm_key = key[:-2]
            storms.setdefault(storm_key, {})[field] = value
        return storms

    def get_warbands(self, lang):
        warbands = [k.copy() for k in self.kingdoms.values()
                    if 'WARBAND' in k['reference_name']
                    and k['colors']
                    ]
        warband_teams = self.user_data['pEconomyModel']['WarbandTeams']
        available_warbands = [warband_teams[str(w)][0]['Data'] for w in self.user_data['pShopWarbandsData']]
        for warband in warbands:
            self.translate_kingdom(warband, lang)
            if ':' in warband['name']:
                warband['name'] = warband['name'].split(':')[1].strip()
            warband['available'] = ''
            if warband['id'] in available_warbands:
                warband['available'] = _('[AVAILABLE]', lang)
        return warbands

    def get_banners(self, lang):
        banners = [k.copy() for k in self.kingdoms.values() if k.get('colors')]
        for banner in banners:
            self.translate_kingdom(banner, lang)
        return sorted(banners, key=lambda x: x['banner']['name'])

    def get_map_data(self, lang, location):
        if not location:
            location = 'krystara'
        base_folder = 'Worldmap'
        map_data = {
            'krystara': {
                'title': _('[MAPNAME_MAIN]', lang),
                'map': f'{base_folder}/Main/Main_Albedo_full.png',
                'water': f'{base_folder}/Main/Water_Main_Albedo_full.png',
                'height': f'{base_folder}/Main/Main_Height_full.png',
                'blend_mode': 'overlay',
            },
            'underworld': {
                'title': _('[MAPNAME_UNDERWORLD]', lang),
                'map': f'{base_folder}/Underworld/Underworld_Albedo_full.png',
                'water': f'{base_folder}/Underworld/Water_Underworld_Albedo_full.png',
                'height': f'{base_folder}/Underworld/Underworld_Height_full.png',
                'blend_mode': 'stereo',
            }
        }
        result = map_data[location]
        result['kingdoms'] = []
        result['title'] = f"Gary's Gems of War Map: {result['title']}"

        def is_pseudo_kingdom(k):
            return k['location'] == 'krystara' and k['links'] == {-1}

        for kingdom in self.kingdoms.values():
            if 'description' not in kingdom:
                continue
            if kingdom['location'] != location:
                continue
            if is_pseudo_kingdom(kingdom):
                continue
            my_kingdom = kingdom.copy()
            self.translate_kingdom(my_kingdom, lang)
            if self.is_untranslated(my_kingdom['name']):
                continue
            result['kingdoms'].append(my_kingdom)
        return result

    def get_weekly_summary(self, lang, emojis):
        world_event = self.get_current_event(lang, emojis)

        def get_single_event(event_type, weekday):
            if filtered_events := [e for e in self.events
                                   if e['type'] == event_type
                                      and e['start_time'] >= world_event['start']
                                      and e['end_time'] <= world_event['end']
                                      and e['start_time'].weekday() == weekday
                                   ]:
                return self.translate_event(filtered_events[0], lang)
            else:
                return {'type': _(event_type, lang), 'start': datetime.datetime.utcnow() - datetime.timedelta(hours=24)}

        weekend_events = [e for e in self.events
                          if e['start_time'].weekday() == 4
                          and e['end_time'].weekday() == 0
                          and e['end_time'] == world_event['end']
                          ]
        extra_events = [
            self.translate_event(e, lang) for e in self.events if
            e['start_time'] == world_event['start'] and
            e['end_time'] == world_event['end'] and
            e['type'] != '[WEEKLY_EVENT]' and
            e['id'] != world_event['id']
        ]
        saturday_pet = [e for e in self.events if e['type'] == '[PETRESCUE]' and e['start_time'].weekday() == 5]
        if saturday_pet:
            saturday_pet = self.translate_event(saturday_pet[0], lang)

        glory_shops = [e for e in self.store_data.values() if e['tab'] == 'WeeklyEvent' and e['currency'] == '[GLORY]']
        glory_costs = None
        glory_rewards = []
        if glory_shops:
            glory_shop = glory_shops[0]
            glory_costs = glory_shop['cost']
            glory_rewards = [gw.copy() for gw in glory_shop['rewards']]
            for reward in glory_rewards:
                reward['name'] = _(reward['name'], lang)

        event_kingdom = self.search_kingdom(str(self.event_key_drops['kingdom_id']), lang)[0]
        event_mythics = [troop for troop in event_kingdom['troops']
                         if troop['raw_rarity'] == 'Mythic'
                         and 'release_date' not in troop
                         and 'Boss' not in troop['raw_types']
                         and troop['id'] not in SOULFORGE_ALWAYS_AVAILABLE]

        event_chest_drops = {
            'troops': event_mythics,
            'kingdom': event_kingdom,
        }

        result = {
            'world_event': world_event,
            'extra_events': extra_events,
            'class_trial': get_single_event('[CLASS_EVENT]', 3),
            'pet_rescue': get_single_event('[PETRESCUE]', 2),
            'saturday_pet': saturday_pet,
            'faction_assault': get_single_event('[DELVE_EVENT]', 1),
            'weekend': self.translate_event(weekend_events[0], lang) if weekend_events else None,
            'glory_cost': glory_costs,
            'glory_rewards': glory_rewards,
            'event_chest_drops': event_chest_drops,
            'world_event_title': _('[WEEKLY_EVENT]', lang),
            'restrictions_title': _('[TROOP_RESTRICTIONS]', lang),
            'event_keys_title': _('[KEYTYPE_3_TITLE]', lang),
            'today_weekday': (datetime.datetime.utcnow() + datetime.timedelta(
                hours=CONFIG.get('data_shift_hours'))).weekday(),
            'glory_shop_title': f'{_("[GLORY]", lang)} {_("[SHOP]", lang)}',
            'kingdom_title': _('[KINGDOM]', lang),
            'event_ended': _('[EVENT_HAS_ENDED]', lang),
            'medal': _('[REWARD_HELP_HEADING_MEDAL_2]', lang),
            'troop_title': _('[TROOP]', lang),
            'flight_school': _('[FLIGHT_SCHOOL]', lang),
            'last_reward_points': _('[LAST_REWARD]', lang).format(),
            'weapon_title': _('[WEAPON]', lang),
        }
        return result

    def faction_summary(self, lang):
        factions = [k.copy() for k in self.kingdoms.values() if k['underworld'] and k['troop_ids']]
        [self.translate_kingdom(f, lang) for f in factions]
        return sorted(factions, key=operator.itemgetter('name'))

    def get_hoard_potions(self, lang):
        potions = [p.copy() for p in self.hoard_potions.values()]
        for potion in potions:
            potion['traits'] = self.enrich_traits(potion['traits'], lang, delve_id=potion['id'])
            potion['name'] = _(potion['name'], lang)
            potion['description'] = _(potion['description'], lang)
        return potions

    def translate_pet_rescue_stats(self, raw_stats, lang):
        total_rescues = sum(r['rescues'] for r in raw_stats)
        stats = []
        for row in raw_stats:
            pet = self.pets[row['pet_id']].translations[lang]
            amount = row['rescues']
            percentage = 100 * amount / total_rescues
            stats.append([pet, amount, percentage])
        return sorted(stats, key=operator.itemgetter(2, 1), reverse=True), total_rescues

    @staticmethod
    def get_dungeon_altars(lang):
        return [
            {
                'name': _(f'[DUNGEON_TITLE_ALTAR_{i}]', lang),
                'description': _(f'[DUNGEON_DESCRIPTION_ALTAR_{i}]', lang),
            }
            for i in range(7)
        ]

    @staticmethod
    def get_dungeon_traps(lang):
        return [
            {
                'name': _(f'[DUNGEON_TITLE_TRAP_{i}]', lang),
                'description': _(f'[DUNGEON_DESCRIPTION_TRAP_{i}]', lang),
            }
            for i in range(11)
        ]

    def get_orbs(self, lang):
        result = defaultdict(list)
        for orb in self.orbs.values():
            group_name = _(orb["group"], lang)
            result[group_name].append(
                {
                    'name': _(orb['name'], lang),
                    'help': _(orb['help'], lang).replace('%1', f'`{orb["data"]}`'),
                    'chance': f'{orb["chance"]:0.0%}',
                    'emoji': orb['emoji']
                }
            )
        return result

    def get_medals(self, lang):
        medal_id = 20000
        result = {
            'badges': [],
            'medals': [],
            'medals_title': _('[MEDALS]', lang),
            'badges_title': _('[REWARD_HELP_HEADING_MEDAL_1]', lang),
        }
        while True:
            if f'[WONDER_{medal_id}_NAME]' not in t.all_translations[lang]:
                break
            result['badges'].append({
                'name': _(f'[WONDER_{medal_id + 1}_NAME]', lang),
                'description': _(f'[WONDER_{medal_id + 1}_DESC]', lang)
            })
            result['medals'].append({
                'name': _(f'[WONDER_{medal_id + 2}_NAME]', lang),
                'description': _(f'[WONDER_{medal_id + 2}_DESC]', lang)
            })
            medal_id += 3
        result['badges'] = list(batched(result['badges'], 10))
        result['medals'] = list(batched(result['medals'], 10))
        return result