Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[tool.poetry]
name = "Deadbot"
version = "1.11.2"
version = "1.12.0"
description = "Bot that lives to serve deadlock.wiki"
readme = "README.md"
authors=[]
Expand Down
46 changes: 46 additions & 0 deletions src/parser/maps.py
Original file line number Diff line number Diff line change
Expand Up @@ -201,6 +201,52 @@ def override_localization(attr):
}


def class_to_scale_type(class_str: str) -> str | None:
"""
Infer the human-readable scale type from a _class string.
Returns a value like 'spirit', 'range', 'duration', 'cooldown', etc.
"""
if not class_str:
return None

# Pattern 1: scale_function_<suffix>
if class_str.startswith('scale_function_'):
suffix = class_str[len('scale_function_') :]
# Map suffix to SCALE_TYPE_MAP key, then to human type
suffix_to_enum = {
'tech_damage': 'ETechPower',
'tech_range': 'ETechRange',
'tech_duration': 'ETechDuration',
'tech_cooldown': 'ETechCooldown',
'weapon_damage': 'EWeaponDamageScale',
'ability_charges': 'EMaxChargesIncrease',
'ability_recharge_time': 'ETechCooldown', # cooldown between charges
}
enum_key = suffix_to_enum.get(suffix)
if enum_key:
return SCALE_TYPE_MAP.get(enum_key)

# Pattern 2: C<Name>ScaleFunction (e.g., CTechPowerScaleFunction)
elif class_str.startswith('C') and class_str.endswith('ScaleFunction'):
middle = class_str[1 : -len('ScaleFunction')] # e.g., 'TechPower'
enum_key = 'E' + middle
return SCALE_TYPE_MAP.get(enum_key)

return None


def class_to_scale_enum(class_str: str) -> str | None:
"""Return the enum key (e.g., 'ETechRange') from a _class string."""
human_type = class_to_scale_type(class_str)
if not human_type:
return None
# Reverse lookup in SCALE_TYPE_MAP
for enum_key, human in SCALE_TYPE_MAP.items():
if human == human_type:
return enum_key
return None


def get_scale_type(scale):
if scale is None:
return scale
Expand Down
8 changes: 8 additions & 0 deletions src/parser/parser.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
generics,
npc_units,
game_map,
misc,
)
from utils import json_utils
from loguru import logger
Expand Down Expand Up @@ -148,6 +149,7 @@ def run(self):
self._parse_localizations()
self._parse_soul_unlocks()
self._parse_generics()
self._parse_misc()
self._parse_map()
logger.trace('Done parsing')

Expand Down Expand Up @@ -309,3 +311,9 @@ def _generate_resource_lookup(self, parsed_heroes, parsed_abilities, parsed_item
).run()

json_utils.write(self.OUTPUT_DIR + '/json/resource-lookup.json', lookup)

def _parse_misc(self):
logger.trace('Parsing Misc...')
parsed_misc = misc.MiscParser(self.data['scripts']['misc']).run()

json_utils.write(self.OUTPUT_DIR + '/json/misc-data.json', json_utils.sort_dict(parsed_misc))
41 changes: 26 additions & 15 deletions src/parser/parsers/abilities/__main__.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
import utils.json_utils as json_utils
import utils.num_utils as num_utils
from .upgrades import parse_upgrades
from .modifiers import parse_modifiers
from loguru import logger


Expand Down Expand Up @@ -54,10 +55,8 @@ def _parse_ability(self, ability_key):
else:
ability_data[key] = value

if 'm_vecAbilityUpgrades' in ability:
ability_data['Upgrades'] = parse_upgrades(ability)
else:
ability_data['Upgrades'] = []
ability_data.update(parse_upgrades(ability))
ability_data.update(parse_modifiers(ability))

formatted_ability_data = {}
for attr_key, attr_value in ability_data.items():
Expand All @@ -82,14 +81,26 @@ def _get_scale(self, stat):
Get scale data for the ability attribute, which will refer to how the value of the attribute
scales with another stat, usually Spirit
"""
if 'm_subclassScaleFunction' in stat:
scale = stat['m_subclassScaleFunction']
# Only include scale with a value, as not sure what
# any others mean so far.
if 'm_flStatScale' in scale:
return {
'Value': scale['m_flStatScale'],
'Type': maps.get_scale_type(scale.get('m_eSpecificStatScaleType', 'ETechPower')),
}

return
if 'm_subclassScaleFunction' not in stat:
return
scale = stat['m_subclassScaleFunction']
if 'm_flStatScale' not in scale:
return

scale_type = scale.get('m_eSpecificStatScaleType')
human_type = None
if scale_type:
human_type = maps.get_scale_type(scale_type)
else:
# Fallback: infer from _class
class_str = scale.get('_class', '')
if class_str:
human_type = maps.class_to_scale_type(class_str)
if not human_type:
# Last resort: default to spirit
human_type = maps.get_scale_type('ETechPower')

return {
'Value': scale['m_flStatScale'],
'Type': human_type,
}
91 changes: 91 additions & 0 deletions src/parser/parsers/abilities/modifiers.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
import re


_PREFIX_RE = re.compile(r'[A-Z]')
_MODIFIER_VALUES_KEY = 'm_vecAutoRegisterModifierValueFromAbilityPropertyName'


def parse_modifiers(ability: dict) -> dict:
"""Walk an ability and extract a nested modifier hierarchy.

Any key whose name contains 'modifier' (case-insensitive).
The output uses the same keys as source but strips the prefixes
(e.g. m_DebuffModifier -> DebuffModifier,
m_AutoIntrinsicModifiers -> AutoIntrinsicModifiers).

From each modifier node, copy:
- _class -> Class (verbatim, only if a non-empty string)
- _my_subclass_name -> Subclass (verbatim, only if a non-empty string)
- m_vecAutoRegisterModifierValueFromAbilityPropertyName ->
AutoRegisterModifierValueFromAbilityPropertyName (verbatim list of
property names, only if non-empty).
- direct int/float children -> key stripped of its lowercase prefix
(e.g. m_flDuration -> Duration), value verbatim.
- nested modifier-named children, recursively (same recursion gate).

Nodes that end up with no fields at all (only empty class etc.) are omitted.
"""
return _parse_dict_modifiers(ability)


def _strip_prefix(key: str) -> str:
match = _PREFIX_RE.search(key)
if match:
return key[match.start() :]
return key


def _parse_dict_modifiers(node: dict) -> dict:
out = {}
for k, v in node.items():
if 'modifier' not in k.lower():
continue
parsed = _parse_modifier_value(v)
if parsed is not None:
out[_strip_prefix(k)] = parsed
return out


def _parse_modifier_value(value):
if isinstance(value, dict):
return _parse_modifier_node(value)
if isinstance(value, list):
items = []
for item in value:
if not isinstance(item, dict):
continue
parsed = _parse_modifier_node(item)
if parsed is not None:
items.append(parsed)
return items if items else None
return None


def _parse_modifier_node(node: dict) -> dict | None:
out = {}

cls = node.get('_class')
if isinstance(cls, str) and cls:
out['Class'] = cls
subclass = node.get('_my_subclass_name')
if isinstance(subclass, str) and subclass:
out['Subclass'] = subclass

auto_register = node.get(_MODIFIER_VALUES_KEY)
if isinstance(auto_register, list) and auto_register:
out[_strip_prefix(_MODIFIER_VALUES_KEY)] = auto_register

for k, v in node.items():
if k in ('_class', '_my_subclass_name', _MODIFIER_VALUES_KEY):
continue
if isinstance(v, bool):
continue
if isinstance(v, (int, float)):
out[_strip_prefix(k)] = v
continue
if 'modifier' in k.lower():
parsed = _parse_modifier_value(v)
if parsed is not None:
out[_strip_prefix(k)] = parsed

return out or None
34 changes: 24 additions & 10 deletions src/parser/parsers/abilities/upgrades.py
Original file line number Diff line number Diff line change
@@ -1,17 +1,19 @@
from parser import maps
from . import utils
from utils import json_utils, num_utils
from typing import TypedDict
from typing import NotRequired, TypedDict


class ScaleEntry(TypedDict):
Value: int | float
Type: str | None
Multiply: NotRequired[bool]


class UpgradeWithScale(TypedDict):
Value: int | float
Scale: ScaleEntry | list[ScaleEntry]
Multiply: NotRequired[bool]


ParsedUpgradeValue = int | float | UpgradeWithScale
Expand All @@ -31,7 +33,9 @@ class RawUpgrade(TypedDict):
SCALE_UPGRADE_TYPES = ['EAddToScale', 'EMultiplyScale']


def parse_upgrades(ability):
def parse_upgrades(ability: dict) -> dict:
if 'm_vecAbilityUpgrades' not in ability:
return {'Upgrades': []}
upgrade_sets = ability['m_vecAbilityUpgrades']
parsed_upgrade_sets = []

Expand Down Expand Up @@ -89,9 +93,11 @@ def parse_upgrades(ability):
if base_stat and 'm_subclassScaleFunction' in base_stat:
scale_func = base_stat['m_subclassScaleFunction']
scale_type = scale_func.get('m_eSpecificStatScaleType')
if scale_type is None and 'tech' in scale_func.get('_class', ''):
scale_type = 'ETechPower'
scale = {
if scale_type is None:
class_str = scale_func.get('_class', '')
if class_str:
scale_type = maps.class_to_scale_enum(class_str)
scale: ScaleEntry = {
'Value': entry['value'],
'Type': maps.get_scale_type(scale_type),
}
Expand All @@ -103,16 +109,24 @@ def parse_upgrades(ability):
raise Exception(f'Unhandled upgrade type {entry["upgrade_type"]}')

if scales:
parsed_upgrade: ParsedUpgradeValue = {}
# if there are multiple scales for a prop, output as an array
if len(scales) > 1:
parsed_upgrade_set[prop] = {'Value': base_value, 'Scale': scales}
parsed_upgrade = {'Value': base_value, 'Scale': scales}
else:
parsed_upgrade_set[prop] = {'Value': base_value, 'Scale': scales[0]}
parsed_upgrade = {'Value': base_value, 'Scale': scales[0]}

if multiply_base:
parsed_upgrade_set[prop]['Multiply'] = True
parsed_upgrade['Multiply'] = True
# convert the game's percentage increase to a simple multiplication
# as this convention is used by multiply of scale upgrade
# eg. 113 -> 1.13 + 1 = 2.13
parsed_upgrade['Value'] = base_value / 100 + 1
else:
parsed_upgrade_set[prop] = base_value
parsed_upgrade = base_value

parsed_upgrade_set[prop] = parsed_upgrade

parsed_upgrade_sets.append(parsed_upgrade_set)

return parsed_upgrade_sets
return {'Upgrades': parsed_upgrade_sets}
12 changes: 9 additions & 3 deletions src/parser/parsers/items.py
Original file line number Diff line number Diff line change
Expand Up @@ -172,14 +172,20 @@ def _extract_scaling(self, attr: Dict[str, Any], item_key: str, attr_key: str) -
return None

scale_type = scale_func.get('m_eSpecificStatScaleType')
human_type = get_scale_type(scale_type)
human_type = get_scale_type(scale_type) if scale_type else None

# Fallback to inferring from _class
if not human_type:
return None
class_str = scale_func.get('_class', '')
if class_str:
human_type = maps.class_to_scale_type(class_str)
if not human_type:
return None

try:
base_value = num_utils.assert_number(base_value_str)
scale_value = num_utils.assert_number(raw_scale_value)
if math.isnan(scale_value) or math.isinf(scale_value):
if math.isnan(scale_value) or math.isinf(scale_value) or scale_value == 0:
return None
except (ValueError, TypeError):
return None
Expand Down
Loading
Loading