maduck/GoWDiscordTeamBot

View on GitHub
data_source/game_data.py

Summary

Maintainability
F
6 days
Test Coverage
import datetime
import math
import operator
import re

from configurations import CONFIG
from data_source import Pets
from event_helpers import extract_currencies, extract_lore, extract_name, get_first_battles, roles_translation, \
    transform_battle
from game_assets import GameAssets
from game_constants import COLORS, COST_TYPES, EVENT_TYPES, GEM_TUTORIAL_IDS, OrbType, RewardTypes, \
    SOULFORGE_ALWAYS_AVAILABLE
from game_constants.soulforge import NON_CRAFTABLE_WEAPON_IDS
from util import U, convert_color_array

FIRST_ARCANE_TRAITSTONE_INDEX = 18

NO_TRAIT = {'code': '', 'name': '[TRAIT_NONE]', 'description': '[TRAIT_NONE_DESC]'}


class GameData:

    def __init__(self):
        self.data = None
        self.user_data = {
            'pEconomyModel': {
                'TroopReleaseDates': [],
                'KingdomReleaseDates': [],
                'HeroClassReleaseDates': [],
                'PetReleaseDates': [],
                'RoomReleaseDates': [],
                'WeaponReleaseDates': [],
                'BasicLiveEventArray': [],
            }
        }

        self.troops = {'`?`': {'name': '`?`', 'color_code': 'questionmark'}}
        self.troop_types = set()
        self.spells = {}
        self.effects = set()
        self.positive_effects = {'BARRIER', 'ENCHANTED', 'ENRAGED', 'SUBMERGED', 'BLESSED', 'MIRROR'}
        self.weapons = {}
        self.classes = {}
        self.banners = {}
        self.traits = {}
        self.kingdoms = {
            '`?`': {'name': '[REQUIREMENTS_NOT_MET]', 'underworld': False, 'filename': None, 'id': '`?`',
                    'location': None, 'reference_name': '`?`'}}
        self.pet_effects = ()
        self.pets: Pets = None
        self.talent_trees = {}
        self.spoilers = []
        self.events = []
        self.soulforge_weapons = []
        self.campaign_tasks = {}
        self.campaign_data = {}
        self.campaign_skip_costs = {}
        self.campaign_rerolls = {}
        self.campaign_week = None
        self.artifact_id = None
        self.campaign_name = ''
        self.soulforge = {}
        self.summons = {}
        self.soulforge_raw_data = {}
        self.traitstones = {}
        self.levels = []
        self.adventure_board = []
        self.drop_chances = {}
        self.event_chest_drops = {}
        self.event_kingdoms = []
        self.event_raw_data = {}
        self.weekly_event = {}
        self.gem_events = {}
        self.store_raw_data = {}
        self.store_data = {}
        self.hoard_potions = {}
        self.orbs = {}

    def read_json_data(self):
        self.data = GameAssets.load('World.json')
        self.user_data = GameAssets.load('User.json')
        if GameAssets.exists('Campaign.json'):
            self.campaign_data = GameAssets.load('Campaign.json')
        if GameAssets.exists('Soulforge.json'):
            self.soulforge_raw_data = GameAssets.load('Soulforge.json')
        if GameAssets.exists('Event.json'):
            self.event_raw_data = GameAssets.load('Event.json')
        if GameAssets.exists('Store.json'):
            self.store_raw_data = GameAssets.load('Store.json')

    def populate_world_data(self):
        self.read_json_data()

        self.populate_spells()
        self.populate_traits()
        self.populate_troops()
        self.pets = Pets(self.data['Pets'], self.user_data, self.troops)
        self.populate_kingdoms()
        self.populate_weapons()
        self.populate_talents()
        self.populate_classes()
        self.populate_release_dates()
        self.enrich_kingdoms()
        self.add_troops_to_kingdoms_by_filename()
        self.populate_campaign_tasks()
        self.populate_soulforge()
        self.populate_traitstones()
        self.populate_hero_levels()
        self.populate_max_power_levels()
        self.populate_adventure_board()
        self.populate_drop_chances()
        self.populate_event_key_drops()
        self.populate_event_kingdoms()
        self.populate_store_data()
        self.populate_weekly_event_details()
        self.populate_gem_events()
        self.populate_hoard_potions()
        self.populate_orbs()

    def populate_classes(self):
        for _class in self.data['HeroClasses']:
            if _class['KingdomId'] not in self.kingdoms:
                _class['KingdomId'] = '`?`'
            self.classes[_class['Id']] = {
                'id': _class['Id'],
                'name': _class['Name'],
                'code': _class['Code'],
                'talents': [self.talent_trees[tree]['talents'] for tree in _class['TalentTrees']],
                'trees': _class['TalentTrees'],
                'traits': [self.traits.get(trait, NO_TRAIT) for trait in
                           _class['Traits']],
                'weapon_id': _class['ClassWeaponId'],
                'kingdom_id': _class['KingdomId'],
                'type': _class['Augment'][0],
                'magic_color': _class['BonusColor'],
                'weapon_color': _class['BonusWeapon'],
            }
            self.weapons[_class['ClassWeaponId']]['class'] = _class['Name']
            for tree in _class['TalentTrees']:
                self.talent_trees[tree]['classes'].append(self.classes[_class['Id']].copy())
            self.kingdoms[_class['KingdomId']]['class_id'] = _class['Id']

    def populate_talents(self):
        for tree in self.data['TalentTrees']:
            talents = [self.traits.get(trait, trait) for trait in tree['Traits']]
            self.talent_trees[tree['Code']] = {
                'name': f'[TALENT_TREE_{tree["Code"].upper()}]',
                'talents': talents,
                'classes': [],
            }

    def populate_weapons(self):
        for weapon in self.data['Weapons']:
            colors = convert_color_array(weapon)
            self.weapons[weapon['Id']] = {
                'id': weapon['Id'],
                'name': f'[SPELL{weapon["SpellId"]}_NAME]',
                'description': f'[SPELL{weapon["SpellId"]}_DESC]',
                'colors': colors,
                'rarity': weapon['WeaponRarity'],
                'type': weapon['Type'],
                'roles': weapon['TroopRoleArray'],
                'spell_id': weapon['SpellId'],
                'kingdom': self.kingdoms[weapon['KingdomId']],
                'kingdom_id': weapon['KingdomId'],
                'requirement': weapon.get('MasteryRequirement', 0),
                'armor_increase': weapon['ArmorIncrease'],
                'attack_increase': weapon['AttackIncrease'],
                'health_increase': weapon['HealthIncrease'],
                'magic_increase': weapon['SpellPowerIncrease'],
                'affixes': [self.spells.get(spell) for spell in weapon['Affixes'] if spell in self.spells],
            }
            self.kingdoms[weapon['KingdomId']]['weapon_ids'].append(weapon['Id'])

    def populate_kingdoms(self):
        for kingdom in self.data['Kingdoms']:
            self.pets.fill_untranslated_kingdom_name(kingdom['Id'], kingdom['ReferenceName'])
            colors = [f'[GEM_{c.upper()}]' for c in COLORS]
            colors = zip(colors, kingdom['BannerColors'])
            colors = sorted(colors, key=operator.itemgetter(1), reverse=True)
            self.banners[kingdom['Id']] = {
                'id': kingdom['Id'],
                'name': kingdom['BannerName'],
                'colors': colors,
                'filename': kingdom['FileBase'],
            }
            kingdom_troops = [troop_id for troop_id in kingdom['TroopIds'] if troop_id != -1]
            for troop_id in kingdom_troops:
                if troop_id in self.troops:
                    self.troops[troop_id]['kingdom_id'] = kingdom['Id']
            kingdom_colors = convert_color_array(kingdom)
            self.kingdoms[kingdom['Id']] = {
                'id': kingdom['Id'],
                'name': kingdom['Name'],
                'description': kingdom['Description'],
                'punchline': kingdom['ByLine'],
                'underworld': bool(kingdom.get('MapIndex', 0)),
                'location': self.infer_kingdom_location(kingdom),
                'troop_ids': kingdom_troops,
                'weapon_ids': [],
                'troop_type': kingdom['KingdomTroopType'],
                'linked_kingdom_id': kingdom.get('SisterKingdomId'),
                'colors': sorted(kingdom_colors),
                'filename': kingdom['FileBase'],
                'reference_name': kingdom['ReferenceName'],
                'coordinates': (kingdom['XPos'], kingdom['YPos']),
                'links': set(kingdom['Links']),
            }
            if 'SisterKingdomId' in kingdom:
                self.kingdoms[kingdom['SisterKingdomId']]['linked_kingdom_id'] = kingdom['Id']
            for troop_id in kingdom_troops:
                if troop_id in self.troops:
                    self.troops[troop_id]['kingdom'] = self.kingdoms[kingdom['Id']]

    @staticmethod
    def infer_kingdom_location(kingdom):
        if 'WARBAND' in kingdom['ReferenceName']:
            return 'warband'
        return 'underworld' if kingdom.get('MapIndex', 0) == 1 else 'krystara'

    def populate_troops(self):
        for troop in self.data['Troops']:
            colors = convert_color_array(troop)
            self.troops[troop['Id']] = {
                'id': troop['Id'],
                'name': troop['Name'],
                'reference_name': troop['ReferenceName'],
                'colors': sorted(colors),
                'description': troop['Description'],
                'spell_id': troop['SpellId'],
                'has_shiny': troop.get('HasShiny', False),
                'shiny_spell_id': troop.get('ShinySpellId'),
                'traits': [self.traits.get(trait, NO_TRAIT) for trait in
                           troop['Traits']],
                'rarity': troop['TroopRarity'],
                'types': [troop['TroopType']],
                'roles': troop['TroopRoleArray'],
                'kingdom': {'name': '', 'reference_name': ''},
                'filename': troop['FileBase'],
                'armor': sum(troop['ArmorIncrease']),
                'health': sum(troop['HealthIncrease']),
                'magic': sum(troop['SpellPowerIncrease']),
                'attack': sum(troop['AttackIncrease']),
            }
            if 'TroopType2' in troop:
                self.troops[troop['Id']]['types'].append(troop['TroopType2'])
            for type_ in self.troops[troop['Id']]['types']:
                self.troop_types.add(type_)

    def populate_traits(self):
        for trait in self.data['Traits']:
            self.traits[trait['Code']] = {
                'code': trait['Code'],
                'name': trait['Name'],
                'description': trait['Description'],
                'image': trait['Image'],
            }

    def populate_spells(self):
        for spell in self.data['Spells']:
            spell_effects = []
            boost = 0
            last_type = ""
            for step in spell['SpellSteps']:
                if step.get('Type', '').lower().startswith('cause'):
                    effect_code = step['Type'].upper().replace('CAUSE', '')
                    self.effects.add(effect_code.upper())
                if 'Type' in step and 'SpellPowerMultiplier' in step:
                    amount = step.get('Amount', 0)
                    multiplier = step.get('SpellPowerMultiplier', 1)
                    if last_type != step['Type']:
                        spell_effects.append([multiplier, amount])
                        last_type = step['Type']
                if step['Type'].startswith('Count') \
                        and not step['Type'].endswith('Max') \
                        and not step['Type'].endswith('Min'):
                    boost = step.get('Amount', 1)
            self.spells[spell['Id']] = {
                'id': spell['Id'],
                'name': spell['Name'],
                'description': spell['Description'],
                'cost': spell.get('Cost', '[N/A]'),
                'effects': spell_effects,
                'boost': boost,
            }

        self.effects -= {'SPECIFICSTATUSEFFECTCONDITIONAL', 'ALLPOSITIVESTATUSEFFECTS', 'ALLNEGATIVESTATUSEFFECTS'}

    def get_current_event_kingdom_id(self):
        if 'CurrentEventKingdomId' in self.user_data['pEconomyModel']:
            return self.user_data['pEconomyModel']['CurrentEventKingdomId']
        today = datetime.date.today()
        weekly_events = [e for e in self.events
                         if e['end'] - e['start'] == datetime.timedelta(days=7)
                         and e['start'] <= today <= e['end']
                         and e['start'].weekday() == 0
                         and e['kingdom_id']
                         and e['type'] == '[WEEKLY_EVENT]']
        return int(weekly_events[0]['kingdom_id']) if weekly_events else 3000

    def get_current_campaign_week(self):
        if self.campaign_week:
            return self.campaign_week
        release_dates = self.user_data['pEconomyModel']['ArtifactReleaseDates']
        now = datetime.datetime.utcnow() + datetime.timedelta(hours=CONFIG.get('data_shift_hours'))
        for release in release_dates:
            artifact_release = datetime.datetime.strptime(release['Date'], '%m/%d/%Y %H:%M:%S %p %Z')
            release_age = now - artifact_release
            if datetime.timedelta(days=0) <= release_age <= datetime.timedelta(days=10 * 7):
                week_no = math.ceil(release_age / datetime.timedelta(days=7))
                self.artifact_id = release['ArtifactId']
                self.campaign_week = week_no
                return week_no
        current_artifact_id = self.user_data['pEconomyModel']['LowestUnpurchasableArtifactId']
        event_kingdom_id = self.get_current_event_kingdom_id()
        week = 1
        for artifact in self.data['Artifacts']:
            if artifact['Id'] != current_artifact_id:
                continue
            for week, level in enumerate(artifact['Levels']):
                if level['KingdomId'] == event_kingdom_id:
                    break
        self.artifact_id = current_artifact_id
        self.campaign_week = week
        return week

    def populate_campaign_tasks(self):
        event_kingdom_id = self.get_current_event_kingdom_id()
        week = self.get_current_campaign_week()
        for artifact in self.data['Artifacts']:
            if artifact['Id'] == self.artifact_id:
                self.campaign_name = artifact['Name']

        tasks = self.user_data['pTasksData']['CampaignTasks'][str(event_kingdom_id)]
        rerolls = self.user_data['pTasksData']['CampaignRerollTasks']

        level_nums = {
            'Gold': 2,
            'Silver': 4,
            'Bronze': 10,
        }

        for level in ('Gold', 'Silver', 'Bronze'):
            task_list = [self.transform_campaign_task(task, week) for task in tasks[level]]
            task_list = sorted(task_list, key=operator.itemgetter('order'))
            task_list = task_list[-level_nums[level]:]
            self.campaign_tasks[level.lower()] = task_list
            reroll_list = [self.transform_campaign_task(task, week) for task in rerolls[f'Campaign{level}']]
            self.campaign_rerolls[level.lower()] = reroll_list
        self.campaign_tasks['kingdom'] = self.kingdoms[event_kingdom_id]
        self.populate_campaign_skip_costs()

    def populate_campaign_skip_costs(self):
        level_names = {
            'CampaignBronze': '[MEDAL_LEVEL_0]',
            'CampaignSilver': '[MEDAL_LEVEL_1]',
            'CampaignGold': '[MEDAL_LEVEL_2]',
        }
        for level, cost in self.user_data['pEconomyModel']['CampaignTaskSkipCost'].items():
            level_name = level_names[level]
            self.campaign_skip_costs[level_name] = cost

    def transform_campaign_task(self, task, week):
        extra_data = {}
        task_order = 0
        kingdom_id = 0
        if 'Reroll' not in task['Id']:
            m = re.match(r'Campaign_(?P<kingdom_id>\d+)_(?P<level>.+)_(?P<order>\d+)', task['Id'])
            task_id = m.groupdict()
            task_order = 1
            kingdom_id = int(task_id['kingdom_id'])
            level = task_id['level']
        else:
            level = task['Id'].split('_')[2]

        for i, t in enumerate(self.campaign_data.get(f'Campaign{level}', [])):
            if t and t['Id'] == task['Id']:
                extra_data = t
                task_order = i

        if task['TaskTitle'].endswith('CRYSTALS]') or (
                task['TaskTitle'] == '[TASK_COLOR_SLAYER]' and 'Reroll' in task['Id']) or (
                task['TaskTitle'] == '[TASK_GRAVE_KEEPER]'):
            extra_data['Value1'] = task['YValue']
        elif task['TaskTitle'] == '[TASK_DEEP_DELVER]':
            extra_data['Value1'] = 10 * (week + 1)
        elif task['TaskTitle'] == '[TASK_INTREPID_EXPLORER]':
            extra_data['Value1'] = week
        elif task['TaskTitle'] == '[TASK_FORGOTTEN_EXPLORER]' and 'Reroll' in task['Id']:
            extra_data['Value1'] = '`Current Campaign Week`'

        return {
            'reward': task['Rewards'][0]['Amount'],
            'condition': task.get('Condition'),
            'order': task_order,
            'task': task['Task'],
            'name': task['TaskName'],
            'title': task['TaskTitle'],
            'tags': task.get('Tag', '').split(','),
            'x': task.get('XValue'),
            'y': task.get('YValue'),
            'value0': U(extra_data.get('Value0', '`?`')),
            'value1': U(extra_data.get('Value1', '`?`')),
            'c': U(task.get('CValue')),
            'd': U(task.get('DValue')),
            'kingdom_id': kingdom_id,
            'orig': task,
        }

    @staticmethod
    def get_datetime(val):
        date_format = '%m/%d/%Y %I:%M:%S %p %Z'
        return datetime.datetime.strptime(val, date_format).replace(tzinfo=datetime.timezone.utc)

    def populate_release_dates(self):
        self.populate_troop_release_dates()
        self.populate_pet_release_dates()
        self.populate_kingdom_release_dates()
        self.populate_class_release_dates()
        self.populate_room_release_dates()
        self.populate_weapon_release_dates()
        self.populate_event_release_dates()
        self.populate_weekly_event_dates()

        self.events.sort(key=operator.itemgetter('start'))
        self.spoilers.sort(key=operator.itemgetter('date'))

    def populate_weekly_event_dates(self):
        week_long_events = [e for e in self.events
                            if e['end'] - e['start'] == datetime.timedelta(days=7)
                            and e['kingdom_id']]
        for event in week_long_events:
            kingdom_weapons = [w['id'] for w in self.weapons.values()
                               if 'kingdom' in w and w['kingdom']['id'] == event['kingdom_id']
                               and w['id'] not in NON_CRAFTABLE_WEAPON_IDS
                               and w.get('release_date', datetime.datetime.min).date() < event['end']]
            self.soulforge_weapons.append({
                'start': event['start'],
                'end': event['end'],
                'weapon_ids': kingdom_weapons,
            })

    def populate_event_release_dates(self):
        for release in self.user_data['BasicLiveEventArray']:
            gacha_troop = release['GachaTroop']
            gacha_troops = release.get('GachaTroops', [])
            result = {'id': release['Id'],
                      'start': datetime.datetime.utcfromtimestamp(release['StartDate']).date(),
                      'start_time': datetime.datetime.utcfromtimestamp(release['StartDate']),
                      'end': datetime.datetime.utcfromtimestamp(release['EndDate']).date(),
                      'end_time': datetime.datetime.utcfromtimestamp(release['EndDate']),
                      'type': EVENT_TYPES.get(release['Type'], release['Type']),
                      'names': release.get('Name'),
                      'gacha': gacha_troop,
                      'troops': gacha_troops,
                      'kingdom_id': release.get('Kingdom'),
                      'artifact_id': release.get('ArtifactId'),
                      }
            if gacha_troop and gacha_troop in self.troops:
                self.troops[gacha_troop]['event'] = True
            self.events.append(result)

    def populate_weapon_release_dates(self):
        for release in self.user_data['pEconomyModel']['WeaponReleaseDates']:
            weapon_id = release['WeaponId']
            release_date = self.get_datetime(release['Date'])
            if weapon_id in self.weapons:
                self.weapons[weapon_id]['release_date'] = release_date
                self.spoilers.append({'type': 'weapon', 'date': release_date, 'id': weapon_id})

    def populate_room_release_dates(self):
        for release in self.user_data['pEconomyModel']['RoomReleaseDates']:
            room_id = release['RoomId']
            release_date = self.get_datetime(release['Date'])
            self.spoilers.append({'type': 'room', 'date': release_date, 'id': room_id})

    def populate_class_release_dates(self):
        for release in self.user_data['pEconomyModel']['HeroClassReleaseDates']:
            class_code = release['ClassCode']
            release_date = self.get_datetime(release['Date'])
            if classes := [
                c['id'] for c in self.classes.values() if c['code'] == class_code
            ]:
                class_id = classes[0]
                self.classes[class_id]['release_date'] = release_date
                self.spoilers.append({'type': 'classe', 'date': release_date, 'id': class_id})

    def populate_kingdom_release_dates(self):
        for release in self.user_data['pEconomyModel']['KingdomReleaseDates']:
            kingdom_id = release['KingdomId']
            release_date = self.get_datetime(release['Date'])
            if kingdom_id in self.kingdoms:
                self.kingdoms[kingdom_id]['release_date'] = release_date
                self.spoilers.append({'type': 'kingdom', 'date': release_date, 'id': kingdom_id})

    def populate_pet_release_dates(self):
        for release in self.user_data['pEconomyModel']['PetReleaseDates']:
            pet_id = release['PetId']
            release_date = self.get_datetime(release['Date'])
            if pet_id in self.pets:
                self.pets[pet_id].set_release_date(release_date)
                self.spoilers.append({'type': 'pet', 'date': release_date, 'id': pet_id})

    def populate_troop_release_dates(self):
        release: dict
        for release in self.user_data['pEconomyModel']['TroopReleaseDates']:
            troop_id = release['TroopId']
            release_date = self.get_datetime(release['Date'])
            if troop_id in self.troops:
                self.troops[troop_id]['release_date'] = release_date
                self.spoilers.append({'type': 'troop', 'date': release_date, 'id': troop_id})
                if self.troops[troop_id]['rarity'] == 'Mythic' and 'Id' in self.troops[troop_id]['kingdom']:
                    self.events.append(
                        {'id': 0,
                         'start': release_date.date(),
                         'end': release_date.date() + datetime.timedelta(days=7),
                         'type': '[RARITY_5]',
                         'names': self.troops[troop_id]['name'],
                         'gacha': troop_id,
                         'kingdom_id': self.troops[troop_id]['kingdom']['Id']}
                    )

    def enrich_kingdoms(self):
        for kingdom_id, kingdom_data in self.user_data['pEconomyModel']['KingdomLevelData'].items():
            self.kingdoms[int(kingdom_id)]['primary_color'] = COLORS[kingdom_data['Color']]
            self.kingdoms[int(kingdom_id)]['primary_stat'] = kingdom_data['Stat']

        for kingdom_id, pet_id in self.user_data['pEconomyModel']['FactionRenownRewardPetIds'].items():
            if pet_id in self.pets:
                self.kingdoms[int(kingdom_id)]['pet'] = self.pets[pet_id]

        factions = [(k_id, kingdom) for k_id, kingdom in self.kingdoms.items() if
                    kingdom['underworld'] and kingdom['troop_ids']]

        faction_weapon_overrides = {
            3053: 1274,
            3054: 1391,
            3069: 1272,
        }
        for faction_id, faction_data in factions:
            kingdom_id = faction_data['linked_kingdom_id']
            if faction_weapons := [
                w['id']
                for w in self.weapons.values()
                if w['kingdom']['id'] == kingdom_id
                   and w['requirement'] == 1000
                   and sorted(w['colors']) == sorted(faction_data['colors'])
                   and w['rarity'] == 'Epic'
            ]:
                weapon_id = faction_weapons[-1]
                weapon_id = faction_weapon_overrides.get(faction_id, weapon_id)
                self.kingdoms[faction_id]['event_weapon'] = self.weapons[weapon_id]
                self.weapons[weapon_id]['event_faction'] = faction_id

    def add_troops_to_kingdoms_by_filename(self):
        pattern = re.compile(r'.+_(?P<filebase>K\d+).*')
        for troop_id, troop in self.troops.items():
            if troop_id == '`?`':
                continue
            kingdom = troop['kingdom']
            if kingdom.get('name') or kingdom.get('reference_name'):
                continue
            match = pattern.match(troop['filename'])
            if not match:
                continue
            kingdom_filename = match["filebase"]
            # Skip Apocalypse (3034) and HoG (3042),
            # because they share filename with
            # Sin of Maraj and Guardians resp. and are
            # unlikely to get new troops
            skip_kingdoms = [3034, 3042]
            if troop_kingdom := next(
                    (
                            k
                            for k in self.kingdoms.values()
                            if k['filename'] == kingdom_filename
                               and k['id'] not in skip_kingdoms
                    ),
                    None,
            ):
                troop['kingdom'] = troop_kingdom
                troop_kingdom['troop_ids'].append(troop_id)

                if troop_kingdom['underworld']:
                    krystara_kingdom_id = self.kingdoms[troop_kingdom['id']]['linked_kingdom_id']
                    self.kingdoms[krystara_kingdom_id]['troop_ids'].append(troop_id)

    def populate_soulforge(self):
        tabs = [
            '[SOULFORGE_TAB_LEVEL]',
            '[SOULFORGE_TAB_JEWELS]',
            '[SOULFORGE_TAB_TRAITSTONES]',
            '[SOULFORGE_TAB_TROOPS]',
            '[SOULFORGE_TAB_WEAPONS]',
            '[SOULFORGE_TAB_OTHER]',
        ]

        recipes = [r for r in self.soulforge_raw_data.get('pRecipeArray', []) if r['Tab'] in (3, 4)]
        for recipe in recipes:
            recipe_id = recipe['Target']['Data']
            if recipe_id < 1000 or recipe_id in SOULFORGE_ALWAYS_AVAILABLE:
                continue
            if not recipe['Name']:
                recipe['Name'] = self.troops[recipe_id]['name']
                recipe['rarity'] = self.troops[recipe_id]['rarity']
            if 'rarity' not in recipe:
                if recipe_id in self.weapons:
                    recipe['rarity'] = self.weapons[recipe_id]['rarity']
                elif recipe_id in self.troops:
                    recipe['rarity'] = self.troops[recipe_id]['rarity']
            category = tabs[recipe['Tab']]
            self.soulforge.setdefault(category, []).append({
                'name': recipe['Name'],
                'id': recipe_id,
                'costs': recipe['Source'],
                'start': recipe['StartDate'],
                'end': recipe['EndDate'],
                'rarity': recipe['rarity'],
            })
        for colour, troops in enumerate(self.soulforge_raw_data.get('pSummonTroopArray', [])):
            stone_name = f'[RECIPE_SUMMONS_{colour}]'
            self.summons[stone_name] = [{'troop_id': troop['nTroopId'], 'count': troop['nQuantity']}
                                        for troop in troops]

    def populate_traitstones(self):
        for traits in self.user_data['pTraitsTable']:
            runes = self.extract_runes(traits['Runes'])
            for rune in runes:
                if rune['name'] in self.traitstones:
                    self.traitstones[rune['name']]['total_amount'] += rune['amount']
                else:
                    self.traitstones[rune['name']] = {
                        'id': rune['id'],
                        'name': rune['name'],
                        'troop_ids': [],
                        'class_ids': [],
                        'kingdom_ids': set(),
                        'total_amount': rune['amount'],
                    }
                if 'ClassCode' in traits:
                    my_class = [_class for _class in self.classes.values()
                                if _class['code'] == traits['ClassCode']]
                    if not my_class:
                        continue
                    class_id = my_class[0]['id']
                    self.classes[class_id]['traitstones'] = runes
                    self.traitstones[rune['name']]['class_ids'].append(class_id)
                elif traits['Troop'] in self.troops:
                    self.troops[traits['Troop']]['traitstones'] = runes
                    self.traitstones[rune['name']]['troop_ids'].append(traits['Troop'])
        for kingdom_id, runes in self.user_data['pEconomyModel']['Explore_RunePerKingdom'].items():
            for rune_id in runes:
                rune_name = self.get_rune_name_from_id(rune_id)
                self.traitstones[rune_name]['kingdom_ids'].add(kingdom_id)
        for kingdom_id, runes in self.user_data['pEconomyModel']['Rune_AfterBattleKingdomData'].items():
            if kingdom_id == '1000':
                continue
            for rune_id in runes:
                rune_name = self.get_rune_name_from_id(rune_id)
                self.traitstones[rune_name]['kingdom_ids'].add(kingdom_id)

    def extract_runes(self, runes):
        result = {}
        for trait in runes:
            for rune in trait:
                rune_id = rune['Id']
                if rune_id in result:
                    result[rune_id]['amount'] += rune['Required']
                else:
                    result[rune_id] = {
                        'id': rune['Id'],
                        'name': self.get_rune_name_from_id(rune['Id']),
                        'amount': rune['Required'],
                    }
        return list(result.values())

    @staticmethod
    def get_rune_name_from_id(rune_id):
        return f'[RUNE{rune_id:02d}_NAME]'

    def populate_hero_levels(self):
        for bonus in self.user_data['pEconomyModel']['HeroLevelUpStats']:
            level_bonus = {
                'level': bonus['Level'],
                'bonus': f'[{bonus["Stat"].upper()}]',
            }
            self.levels.append(level_bonus)

    def populate_max_power_levels(self):
        pattern = re.compile(r'KingdomTask(?P<level>\d+)-.+')
        for kingdom in self.kingdoms.values():
            max_kingdom_level = 0
            for task in self.user_data['pTasksData']['Kingdom']:
                match = pattern.match(task['Id'])
                if not match:
                    print(f'Match is broken for kingdom {kingdom}')
                level = match.groups()[0]
                if not self.kingdom_satisfies_task(kingdom, task):
                    break
                max_kingdom_level = int(level)
            self.kingdoms[kingdom['id']]['max_power_level'] = max_kingdom_level

    def kingdom_satisfies_task(self, kingdom, task):
        valid_ids = [kingdom['id']]
        if kingdom['location'] == 'krystara' and kingdom['linked_kingdom_id']:
            valid_ids.append(kingdom['linked_kingdom_id'])

        def has_enough(items):
            items = [i for i in items.values() if i.get('kingdom_id') in valid_ids]
            now = datetime.datetime.utcnow().replace(tzinfo=datetime.timezone.utc)
            items = [i for i in items if 'release_date' not in i or i['release_date'] <= now]
            return len(items) >= task['XValue']

        def has_enough_new_style(items):
            items = [i.data for i in items.items.values() if i.data.get('kingdom_id') in valid_ids]
            now = datetime.datetime.utcnow().replace(tzinfo=datetime.timezone.utc)
            items = [i for i in items if 'release_date' not in i or i['release_date'] <= now]
            return len(items) >= task['XValue']

        if task['Task'] in ('IncreaseKingdomLevel', 'CompleteQuestline', 'Complete{x}ChallengesIn{y}'):
            return True
        if task['Task'] == 'Own{x}Troops':
            return has_enough(self.troops)
        if task['Task'] == 'Own{x}Weapons':
            return has_enough(self.weapons)
        if task['Task'] == 'Own{x}Classes':
            return has_enough(self.classes)
        if task['Task'] == 'Own{x}Pets':
            return has_enough_new_style(self.pets)
        if task['Task'] == 'Earn{x}Renown':
            return kingdom['linked_kingdom_id'] and not kingdom['underworld']
        return False

    def populate_adventure_board(self):
        for board in self.user_data['pUser']['AdventureData']:
            name = board['Name']
            rewards = self._transform_adventure_reward(board['Battles'])
            rarity = f'[RARITY_{board["Rarity"]}]'
            self.adventure_board.append({
                'name': name,
                'rewards': rewards,
                'rarity': rarity,
                'raw_rarity': int(board['Rarity']),
            })

    def _transform_adventure_reward(self, battles):
        result = {}
        for battle in battles:
            for reward in battle['Rewards']:
                amount = reward['Amount']
                reward_type = self.translate_reward_type(reward)
                result.setdefault(reward_type, 0)
                result[reward_type] += amount
        return result

    def translate_reward_type(self, reward):
        reward_type = f'[{reward["Type"].upper()}]'
        data = reward['Data']
        if reward_type == '[TROOP]':
            data = self.troops.get(data)
        reward_translation = {
            '[GEM]': '[GEMS]',
            '[SOUL]': '[SOULS]',
            '[DEED]': '[DEED{data:02d}]',
            '[RUNE]': '[RUNE{data:02d}_NAME]',
            '[PETFOOD]': '[PETFOOD{data:02d}_NAME]',
            '[KEY]': '[KEYTYPE_{data}_TITLE]',
            '[VAULTKEY]': '[LIVEEVENTENERGY3]',
            '[ORB]': '[REWARD_HELP_HEADING_ORB_{data}]',
            '[DIAMOND]': '[DIAMONDS_GAINED]',
            '[MEDAL]': '[WONDER_{data}_NAME]',
            '[CHATTITLE]': '[TITLE]',
            '[CHATPORTRAIT]': '[PORTRAIT]',
            '[TROOP]': '{data[name]}',
            '[CHAOSSHARD]': '[N_CHAOS_SHARD]',
            '[DEEDBOOK]': '[N_DEEDBOOKS{data:02d}]',
            '[PET]': '[PET_RESCUE_PET]',
        }
        return reward_translation.get(reward_type, reward_type).format(data=data)

    def populate_drop_chances(self):
        for chest_id, chest in self.user_data['ChestInfo'].items():
            if len(chest_id) != 1:
                continue
            drop_chances = chest['DropChances']
            chest_type = f'[KEYTYPE_{chest_id}_TITLE]'
            for drop in drop_chances.values():
                multipliers = [1 for _ in range(len(drop['RarityChance']))]
                multipliers = drop.get('Multiples', multipliers)

                drop_type = f'[{drop["Type"].upper()}]'
                title = drop.get('Title', drop_type)
                if title == '[RUNE]':
                    title = '[SOULFORGE_TAB_TRAITSTONES]'
                self.drop_chances.setdefault(chest_type, {})
                if title in ('[TROOPS]', '[CHESTS_6_HELP_1]'):
                    self.drop_chances[chest_type].setdefault(title, {})
                    self.drop_chances[chest_type][title] = {
                        f'[RARITY_{i}]': {
                            'chance': chance,
                        } if multiple == 1 else {
                            'chance': chance,
                            'multiplier': multiple,
                        }
                        for i, (multiple, chance) in enumerate(zip(multipliers, drop['RarityChance'])) if chance
                    }
                else:
                    self.drop_chances[chest_type].setdefault('[RESOURCES]', {})
                    self.drop_chances[chest_type]['[RESOURCES]'][title] = {'chance': sum(drop['RarityChance'])}

    def populate_event_key_drops(self):
        self.event_chest_drops = {
            'troop_ids': self.user_data['ChestInfo']['3']['DisplayTroopIds'],
            'kingdom_id': self.user_data['ChestInfo']['3']['KingdomId'],
        }

    def populate_event_kingdoms(self):
        current_event_kingdom = self.get_current_event_kingdom_id()
        campaign_events = [e for e in self.events if e['type'] == '[CAMPAIGN]' and e['start'] <= datetime.date.today()]
        if not campaign_events:
            return
        current_artifact_id = campaign_events[0]['artifact_id']
        self.artifact_id = current_artifact_id
        event_kingdoms = []
        for artifact in self.data['Artifacts']:
            if artifact['Id'] < current_artifact_id:
                continue
            current_campaign_week = self.get_current_campaign_week()
            if artifact['Id'] > current_artifact_id:
                current_campaign_week = 1
            event_kingdoms.extend([level['KingdomId'] for level in artifact['Levels']][current_campaign_week:])
            event_kingdoms.append(0)
        if current_event_kingdom in event_kingdoms:
            index = event_kingdoms.index(current_event_kingdom)
            self.event_kingdoms = event_kingdoms[index + 1:]

    def populate_weekly_event_details(self):

        def extract_restrictions(raw_data):

            restrictions = raw_data.get('PlayerTeamRestrictions', {})
            if EVENT_TYPES[raw_data['Type']] == '[TOWER_OF_DOOM]':
                restrictions = {'ManaColors': [raw_data['Color']]}
            elif EVENT_TYPES[raw_data['Type']] == '[JOURNEY]':
                restrictions = {'ManaColors': [raw_data['Color']], 'TroopTypes': [raw_data['TroopType']]}
            return {
                '[FILTER_MANACOLOR]': restrictions.get('ManaColors'),
                '[KINGDOM]': [self.kingdoms[k]['name'] for k in restrictions.get('KingdomIds', [])],
                '[TROOP_TYPES]': restrictions.get('TroopTypes'),
                '[FILTER_WEAPONTYPE]': restrictions.get('WeaponTypes'),
                '[RARITY]': restrictions.get('TroopRarities'),
                '[FILTER_ROLE]': roles_translation(restrictions.get('Roles', [])),
                '[ROSTER]': restrictions.get('RosterIds'),
            }

        def extract_rewards(raw_data):
            rewards = {}
            points = 0
            for i, stage in enumerate(raw_data.get('RewardStageArray', []), start=1):
                rewards[i] = {
                    'points': stage['Total'],
                    'cumulative': points,
                    'rewards': [],
                }
                points += stage['Total']
                for reward in stage.get('RewardArray', []):
                    rewards[i]['rewards'].append(transform_reward(reward))
            return rewards

        def transform_reward(reward):
            reward_type = self.translate_reward_type(reward)
            return {
                'type': reward_type,
                'data': reward['Data'],
                'amount': reward['Amount'],
            }

        def calculate_minimum_tier():
            score_per_member = math.ceil(
                sum(r['points'] for r in self.weekly_event['rewards'].values()) / 30)
            self.weekly_event['score_per_member'] = score_per_member
            minimum_battles = 0
            entry_no = 0
            for i, entry in enumerate(self.event_raw_data['GlobalLeaderboard']):
                if entry['Score'] >= score_per_member:
                    minimum_battles = entry.get('BattlesWon', 1)
                    entry_no = i
            # here basically everybody in the top 100 is over the required score already,
            # that happens later throughout the week.
            if entry_no == len(self.event_raw_data['GlobalLeaderboard']) - 1:
                all_battles = [e.get('BattlesWon', 0) or 0 for e in self.event_raw_data['GlobalLeaderboard']]
                all_scores = [e.get('Score', 0) for e in self.event_raw_data['GlobalLeaderboard']]
                avg_battles = sum(all_battles) / len(all_battles)
                avg_score = sum(all_scores) / len(all_scores)
                minimum_battles = math.ceil(avg_battles * score_per_member / avg_score)

            if EVENT_TYPES.get(self.weekly_event['type']) == '[RAIDBOSS]':
                """
                formula for damage to score conversion is:
                score = 0.2 battles^2 + 7 battles - 220
                
                inverted:
                battles = 1/2 (-sqrt(20 score + 5625) - 35)
                
                boss starts after 10 fights, with a level 20, and increases by 5 each battle.                
                """
                minimum_battles = (minimum_battles - 20) // 5 + 10

            self.weekly_event['minimum_battles'] = minimum_battles
            tier_battles = [62, 67, 75, 81, 94]
            minimum_tier = 5
            for tier, battles in enumerate(tier_battles):
                if minimum_battles and minimum_battles <= battles:
                    minimum_tier = tier
                    break
            if EVENT_TYPES[self.weekly_event['type']] == '[JOURNEY]':
                minimum_tier = max(minimum_tier, 3)
            self.weekly_event['minimum_tier'] = minimum_tier

        battles = [transform_battle(b) for b in self.event_raw_data.get('BattleArray', [])]

        self.weekly_event = {
            'id': self.event_raw_data['Id'],
            'shop_tiers': [self.store_data[gacha] for gacha in self.event_raw_data.get('GachaItems', [])
                           if gacha in self.store_data],
            'kingdom_id': str(self.event_raw_data.get('Kingdom')),
            'type': self.event_raw_data.get('Type'),
            'name': extract_name(self.event_raw_data),
            'lore': extract_lore(self.event_raw_data),
            'restrictions': extract_restrictions(self.event_raw_data),
            'troop_id': self.event_raw_data.get('GachaTroop'),
            'troop': self.troops.get(self.event_raw_data.get('GachaTroop', 6000), {'name': '`?`'})['name']
            if self.event_raw_data.get('GachaTroop') else None,
            'color': COLORS[self.event_raw_data.get('Color')] if 'Color' in self.event_raw_data else None,
            'weapon_id': self.event_raw_data.get('EventWeaponId'),
            'token': self.event_raw_data.get('TokenId'),
            'badge': self.event_raw_data.get('BadgeId'),
            'medal': self.event_raw_data.get('MedalId'),
            'currencies': extract_currencies(self.event_raw_data),
            'rewards': extract_rewards(self.event_raw_data),
            'battles': battles,
            'start': datetime.datetime.utcfromtimestamp(self.event_raw_data['StartDate']),
            'end': datetime.datetime.utcfromtimestamp(self.event_raw_data['EndDate']),
            'first_battles': get_first_battles(self.event_raw_data),
        }
        calculate_minimum_tier()

    def populate_gem_events(self):
        for gem_event in self.user_data['pGemEventData']:
            color = COLORS[gem_event['GemType']]
            self.gem_events[gem_event['Id']] = {
                'event_id': gem_event['Id'],
                'gem_id': gem_event['GemType'],
                'gem_type': color,
                'multiplier': gem_event['Multiplier'],
                'tutorial': GEM_TUTORIAL_IDS.get(color, color),
            }

    def populate_store_data(self):
        for entry in self.store_raw_data['ShopData']:
            if not entry['Visible']:
                continue

            rewards = []
            if entry['RewardType'] == RewardTypes.Bundle:
                rewards.extend(self.extract_reward_bundles(entry))

            self.store_data[entry['Code']] = {
                'title': entry['TitleId'],
                'reference': entry['ReferenceName'],
                'cost': entry['Cost'],
                'currency': COST_TYPES[entry['CostType']],
                'tab': entry.get('Tab'),
                'rewards': rewards,
                'visible': entry.get('Visible') == 'True',
            }

    def extract_reward_bundles(self, entry):
        rewards = []
        for reward in entry.get('BundleData', {}):
            if reward['RewardType'] == RewardTypes.Troop and reward['RewardData'] in self.troops:
                rewards.append({
                    'name': self.troops[reward['RewardData']]['name'],
                    'id': reward['RewardData'],
                    'amount': reward['Reward'],
                })
            elif reward['RewardType'] == RewardTypes.Weapon and reward['RewardData'] in self.weapons:
                rewards.append({
                    'name': self.weapons[reward['RewardData']]['name'],
                    'id': reward['RewardData'],
                    'amount': reward['Reward'],
                })
            elif reward['RewardType'] == RewardTypes.LiveEventPoolTroop:
                rewards.append({
                    'id': 0,
                    'name': '[N_EVENT_POOL_TROOPS]',
                    'amount': reward['Reward'],
                })
            elif reward['RewardType'] == RewardTypes.TraitStones \
                    and reward['RewardData'] >= FIRST_ARCANE_TRAITSTONE_INDEX:
                rewards.append({
                    'id': 0,
                    'name': f'[RUNE{reward["RewardData"]:02d}_NAME]',
                    'amount': reward['Reward'],
                })
            elif reward['RewardType'] == RewardTypes.LiveEventPotion:
                rewards.append({
                    'id': 0,
                    'name': f'[LIVEEVENTPOTION{reward["RewardData"]:02d}_NAME]',
                    'amount': reward['Reward'],
                })
        return rewards

    def populate_hoard_potions(self):
        if 'TreasureHoardPotions' not in self.user_data['pFeatures']:
            return
        for potion in self.user_data['pFeatures']['TreasureHoardPotions']:
            potion_data = self.user_data['pFeatures']['TreasureHoardPotionData'][potion['PotionId']]
            traits = [self.traits[t] for t in potion_data.get('Traits', [])]
            self.hoard_potions[potion['PotionId']] = {
                'id': potion['PotionId'],
                'image': f'potion_{potion["PotionId"]:02d}',
                'name': f'[REWARD_HELP_HEADING_LIVEEVENTPOTION_{potion["PotionId"]}]',
                'description': f'[REWARD_HELP_DESC_LIVEEVENTPOTION_1{potion["PotionId"]}]',
                'level': potion['Level'],
                'recurring': potion['Recurring'],
                'reference_name': potion_data['Name'],
                'traits': traits,
                'skills': potion_data.get('SkillBonuses', []),
            }

    def populate_orbs(self):
        chances = [c[0] if c[0] else c[1] for c in zip(
            self.user_data['pEconomyModel']['ChaosOrbChances'],
            self.user_data['pEconomyModel']['MajorChaosOrbChances']
        )]

        orb_groups = [
            f'[ORB_{i:02d}_NAME]' for i in
            (0, 0, 2, 2, 4, 4, 6, 6, 8, 8, 10, 11, 11, 13, 13, 15, 15, 17, 17, 17)
        ]

        for i, orb in enumerate(OrbType):
            self.orbs[orb] = {
                'code': orb.name,
                'emoji': f'Orbs_Orb_{orb.name}_full',
                'name': f'[ORB_{i:02d}_NAME]',
                'data': self.user_data['pEconomyModel']['OrbPowerIncrements'][i],
                'help': f'[REWARD_HELP_DESC_ORB_{i}]',
                'chance': chances[i],
                'group': orb_groups[i],
            }