From fa8eee307578a563e2f63124bf774afc475f2f4a Mon Sep 17 00:00:00 2001 From: LVL1024 <70866179+LVL1024@users.noreply.github.com> Date: Sat, 23 May 2026 22:14:12 +0000 Subject: [PATCH] feat: extract item investment stats into generic data - Extracts `m_MapModCostBonuses` from selectable heroes - Validates that investment scaling is identical across all heroes - Maps engine slot types to standard categories (Weapon, Vitality, Spirit) - Appends `ItemInvestments` to generic-data.json and adds it to structure validation --- src/parser/parser.py | 50 +++++++++++++++++++++++++++++++++- src/parser/parsers/generics.py | 2 +- 2 files changed, 50 insertions(+), 2 deletions(-) diff --git a/src/parser/parser.py b/src/parser/parser.py index c95ca888..6ab68a61 100644 --- a/src/parser/parser.py +++ b/src/parser/parser.py @@ -1,5 +1,6 @@ import os import shutil +import json from .parsers import ( abilities, @@ -160,10 +161,57 @@ def _parse_soul_unlocks(self): def _parse_generics(self): logger.trace('Parsing Generics...') generic_data_path = self.OUTPUT_DIR + '/json/generic-data.json' - parsed_generics = generics.GenericParser(generic_data_path, self.data['scripts']['generic_data']).run() + generic_data = dict(self.data['scripts']['generic_data']) + generic_data.update(self._extract_item_investment_stats()) + + parsed_generics = generics.GenericParser(generic_data_path, generic_data).run() json_utils.write(generic_data_path, json_utils.sort_dict(parsed_generics)) + INVESTMENT_SLOT_MAP = { + 'EItemSlotType_WeaponMod': 'Weapon', + 'EItemSlotType_Armor': 'Vitality', + 'EItemSlotType_Tech': 'Spirit', + } + + def _extract_item_investment_stats(self) -> dict: + first = None + first_key = None + for hero_key, hero_data in self.data['scripts']['heroes'].items(): + if not isinstance(hero_data, dict): + continue + if hero_key == 'hero_base': + continue + if not hero_data.get('m_bPlayerSelectable', False): + continue + if 'm_MapModCostBonuses' not in hero_data: + # This should not happen for a selectable hero, but we'll skip + logger.warning(f'Selectable hero {hero_key} missing m_MapModCostBonuses') + continue + + remapped = { + self.INVESTMENT_SLOT_MAP[slot]: entries + for slot, entries in hero_data['m_MapModCostBonuses'].items() + if slot in self.INVESTMENT_SLOT_MAP + } + + if first is None: + first = remapped + first_key = hero_key + else: + # deep compare dictionaries (order shouldn't matter because we use same remapping) + if json.dumps(first, sort_keys=True) != json.dumps(remapped, sort_keys=True): + raise ValueError( + f'Item investment stats differ between heroes {first_key} and {hero_key}.\n' f'{first_key}: {first}\n{hero_key}: {remapped}' + ) + + if first is None: + logger.warning('Could not find m_MapModCostBonuses in any selectable hero') + return {} + + logger.trace(f'Extracted item investment stats from {first_key} (validated across all selectable heroes)') + return {'ItemInvestments': first} + def _parse_localizations(self): logger.trace('Parsing Localizations...') return localizations.LocalizationParser(self.localizations, self.OUTPUT_DIR).run() diff --git a/src/parser/parsers/generics.py b/src/parser/parsers/generics.py index 5a671827..7205f2d7 100644 --- a/src/parser/parsers/generics.py +++ b/src/parser/parsers/generics.py @@ -14,7 +14,7 @@ class GenericParser: """ def __init__(self, output_dir, generic_data): - self.STRUCTURE_KEYS_TO_VALIDATE = ['ObjectiveParams', 'RejuvParams', 'ItemPricePerTier'] + self.STRUCTURE_KEYS_TO_VALIDATE = ['ObjectiveParams', 'RejuvParams', 'ItemPricePerTier', 'ItemInvestments'] self.POSSIBLE_PREFIXES = ['m_str', 'm_map', 'm_n', 'm_fl', 'm_', 'fl', 'E', 'n'] self.generic_data_dir = output_dir self.generic_data = generic_data