From c4856dc70188dcc6be323dd9fb3b5b96da37ba17 Mon Sep 17 00:00:00 2001 From: Erik Date: Thu, 9 Apr 2026 12:31:39 +0200 Subject: [PATCH] feat: compute base item values by reversing active spell buffs Extract spell effect mappings from Dictionaries.cs into spell_effects.json. During item ingestion, compute_base_values() reverses active enchantment effects to get true base stats: - base_armor_level: armor without Impenetrability buffs - base_max_damage: damage without Blood Drinker buffs - base_attack_bonus: attack without Heart Seeker buffs - base_melee_defense_bonus: defense without Defender buffs - base_elemental_damage_vs_monsters: elemental without Spirit Drinker - base_mana_conversion_bonus: mana conv without Hermetic Link New columns in ItemCombatStats, exposed in search CTEs. Frontend: Base Armor and Base Dmg columns (hidden by default, toggle on). Requires ALTER TABLE migration before deploy. Co-Authored-By: Claude Opus 4.6 (1M context) --- inventory-service/database.py | 8 ++++ inventory-service/main.py | 66 ++++++++++++++++++++++++++++ inventory-service/spell_effects.json | 63 ++++++++++++++++++++++++++ static/inventory.js | 4 ++ 4 files changed, 141 insertions(+) create mode 100644 inventory-service/spell_effects.json diff --git a/inventory-service/database.py b/inventory-service/database.py index 0592f272..b85c5174 100644 --- a/inventory-service/database.py +++ b/inventory-service/database.py @@ -125,6 +125,14 @@ class ItemCombatStats(Base): gear_pk_damage_rating = Column(Integer) gear_pk_damage_resist_rating = Column(Integer) + # Base values (with active spell buffs reversed) + base_armor_level = Column(Integer) + base_max_damage = Column(Integer) + base_attack_bonus = Column(Float) + base_melee_defense_bonus = Column(Float) + base_elemental_damage_vs_monsters = Column(Float) + base_mana_conversion_bonus = Column(Float) + class ItemRequirements(Base): """Wield requirements and skill prerequisites.""" __tablename__ = 'item_requirements' diff --git a/inventory-service/main.py b/inventory-service/main.py index 9e812b84..54901e4b 100644 --- a/inventory-service/main.py +++ b/inventory-service/main.py @@ -275,6 +275,15 @@ def load_comprehensive_enums(): ENUM_MAPPINGS = load_comprehensive_enums() +# Load spell effect mappings for base value computation +try: + with open("spell_effects.json", "r") as _sef: + SPELL_EFFECTS = json.load(_sef) + logger.info(f"Loaded spell effects: {len(SPELL_EFFECTS.get('int_effects', {}))} int, {len(SPELL_EFFECTS.get('double_effects', {}))} double") +except Exception as e: + logger.warning(f"Failed to load spell_effects.json: {e}") + SPELL_EFFECTS = {"int_effects": {}, "double_effects": {}} + # Share enum mappings with helpers module for suitbuilder helpers.set_enum_mappings(ENUM_MAPPINGS) @@ -1321,6 +1330,56 @@ def get_comprehensive_translations(item_data: Dict[str, Any]) -> Dict[str, Any]: return translations +def compute_base_values(item_data: Dict[str, Any], combat_props: Dict[str, Any]) -> Dict[str, Any]: + """Reverse active spell buffs to get base item values. + + Replicates C# GetBuffedIntValueKey/GetBuffedDoubleValueKey logic: + - Subtract Change values for ActiveSpells (remove temporary buffs) + - Add Bonus values for Spells (item's inherent spell effects) + """ + spells = [int(s) if isinstance(s, (int, float)) else s for s in item_data.get("Spells", [])] + active_spells = [int(s) if isinstance(s, (int, float)) else s for s in item_data.get("ActiveSpells", [])] + + int_effects = SPELL_EFFECTS.get("int_effects", {}) + double_effects = SPELL_EFFECTS.get("double_effects", {}) + + base = {} + + # Integer properties: MaxDamage (218103842), ArmorLevel (28) + for prop, key_id in [("max_damage", 218103842), ("armor_level", 28)]: + value = combat_props.get(prop) + if value is None or value == -1: + continue + for spell_id in active_spells: + effect = int_effects.get(str(spell_id)) + if effect and effect["key"] == key_id: + value -= effect["change"] + for spell_id in spells: + effect = int_effects.get(str(spell_id)) + if effect and effect["key"] == key_id and effect.get("bonus", 0) != 0: + value += effect["bonus"] + base[f"base_{prop}"] = value + + # Double properties: AttackBonus (167772172), MeleeDefenseBonus (29), + # ElementalDmgVsMonsters (152), ManaCBonus (144) + for prop, key_id in [("attack_bonus", 167772172), ("melee_defense_bonus", 29), + ("elemental_damage_vs_monsters", 152), ("mana_conversion_bonus", 144)]: + value = combat_props.get(prop) + if value is None or value == -1.0: + continue + for spell_id in active_spells: + effect = double_effects.get(str(spell_id)) + if effect and effect["key"] == key_id: + value -= effect["change"] + for spell_id in spells: + effect = double_effects.get(str(spell_id)) + if effect and effect["key"] == key_id and effect.get("bonus", 0) != 0: + value += effect["bonus"] + base[f"base_{prop}"] = round(value, 4) + + return base + + def extract_item_properties(item_data: Dict[str, Any]) -> Dict[str, Any]: """Extract and categorize item properties from raw JSON.""" @@ -1607,6 +1666,11 @@ def extract_item_properties(item_data: Dict[str, Any]) -> Dict[str, Any]: if mana_info: properties["mana_info"] = mana_info + # Compute base values by reversing active spell buffs + base_values = compute_base_values(item_data, properties.get("combat", {})) + if base_values: + properties["combat"].update(base_values) + return properties @@ -3013,6 +3077,8 @@ async def search_items( COALESCE(cs.attack_bonus, -1.0) as attack_bonus, COALESCE(cs.melee_defense_bonus, -1.0) as melee_defense_bonus, COALESCE(cs.weapon_time, -1) as weapon_time, + COALESCE(cs.base_armor_level, cs.armor_level, -1) as base_armor_level, + COALESCE(cs.base_max_damage, cs.max_damage, -1) as base_max_damage, GREATEST( COALESCE((rd.int_values->>'314')::int, -1), COALESCE((rd.int_values->>'374')::int, -1) diff --git a/inventory-service/spell_effects.json b/inventory-service/spell_effects.json new file mode 100644 index 00000000..a4ef3def --- /dev/null +++ b/inventory-service/spell_effects.json @@ -0,0 +1,63 @@ +{ + "_comment": "Spell effect mappings extracted from MosswartMassacre/Shared/Constants/Dictionaries.cs. Used to reverse-engineer base item values from buffed values.", + "int_effects": { + "_key_names": {"218103842": "MaxDamage", "28": "ArmorLevel"}, + "1616": {"key": 218103842, "change": 20, "bonus": 0, "name": "Blood Drinker VI"}, + "2096": {"key": 218103842, "change": 22, "bonus": 0, "name": "Infected Caress"}, + "5183": {"key": 218103842, "change": 24, "bonus": 0, "name": "Incantation of Blood Drinker"}, + "4395": {"key": 218103842, "change": 24, "bonus": 0, "name": "Incantation of Blood Drinker"}, + "2598": {"key": 218103842, "change": 2, "bonus": 2, "name": "Minor Blood Thirst"}, + "2586": {"key": 218103842, "change": 4, "bonus": 4, "name": "Major Blood Thirst"}, + "4661": {"key": 218103842, "change": 7, "bonus": 7, "name": "Epic Blood Thirst"}, + "6089": {"key": 218103842, "change": 10, "bonus": 10, "name": "Legendary Blood Thirst"}, + "3688": {"key": 218103842, "change": 300, "bonus": 0, "name": "Prodigal Blood Drinker"}, + + "1486": {"key": 28, "change": 200, "bonus": 0, "name": "Impenetrability VI"}, + "2108": {"key": 28, "change": 220, "bonus": 0, "name": "Brogard's Defiance"}, + "4407": {"key": 28, "change": 240, "bonus": 0, "name": "Incantation of Impenetrability"}, + "2604": {"key": 28, "change": 20, "bonus": 20, "name": "Minor Impenetrability"}, + "2592": {"key": 28, "change": 40, "bonus": 40, "name": "Major Impenetrability"}, + "4667": {"key": 28, "change": 60, "bonus": 60, "name": "Epic Impenetrability"}, + "6095": {"key": 28, "change": 80, "bonus": 80, "name": "Legendary Impenetrability"} + }, + "double_effects": { + "_key_names": {"152": "ElementalDamageVsMonsters", "167772172": "AttackBonus", "29": "MeleeDefenseBonus", "144": "ManaCBonus"}, + "3258": {"key": 152, "change": 0.06, "bonus": 0, "name": "Spirit Drinker VI"}, + "3259": {"key": 152, "change": 0.07, "bonus": 0, "name": "Infected Spirit Caress"}, + "5182": {"key": 152, "change": 0.08, "bonus": 0, "name": "Incantation of Spirit Drinker"}, + "4414": {"key": 152, "change": 0.08, "bonus": 0, "name": "Incantation of Spirit Drinker"}, + "3251": {"key": 152, "change": 0.01, "bonus": 0.01, "name": "Minor Spirit Thirst"}, + "3250": {"key": 152, "change": 0.03, "bonus": 0.03, "name": "Major Spirit Thirst"}, + "4670": {"key": 152, "change": 0.05, "bonus": 0.05, "name": "Epic Spirit Thirst"}, + "6098": {"key": 152, "change": 0.07, "bonus": 0.07, "name": "Legendary Spirit Thirst"}, + "3735": {"key": 152, "change": 0.15, "bonus": 0, "name": "Prodigal Spirit Drinker"}, + + "1592": {"key": 167772172, "change": 0.15, "bonus": 0, "name": "Heart Seeker VI"}, + "2106": {"key": 167772172, "change": 0.17, "bonus": 0, "name": "Elysa's Sight"}, + "4405": {"key": 167772172, "change": 0.20, "bonus": 0, "name": "Incantation of Heart Seeker"}, + "2603": {"key": 167772172, "change": 0.03, "bonus": 0.03, "name": "Minor Heart Thirst"}, + "2591": {"key": 167772172, "change": 0.05, "bonus": 0.05, "name": "Major Heart Thirst"}, + "4666": {"key": 167772172, "change": 0.07, "bonus": 0.07, "name": "Epic Heart Thirst"}, + "6094": {"key": 167772172, "change": 0.09, "bonus": 0.09, "name": "Legendary Heart Thirst"}, + + "1605": {"key": 29, "change": 0.15, "bonus": 0, "name": "Defender VI"}, + "2101": {"key": 29, "change": 0.17, "bonus": 0, "name": "Cragstone's Will"}, + "4400": {"key": 29, "change": 0.20, "bonus": 0, "name": "Incantation of Defender"}, + "2600": {"key": 29, "change": 0.03, "bonus": 0.03, "name": "Minor Defender"}, + "3985": {"key": 29, "change": 0.04, "bonus": 0.04, "name": "Mukkir Sense"}, + "2588": {"key": 29, "change": 0.05, "bonus": 0.05, "name": "Major Defender"}, + "4663": {"key": 29, "change": 0.07, "bonus": 0.07, "name": "Epic Defender"}, + "6091": {"key": 29, "change": 0.09, "bonus": 0.09, "name": "Legendary Defender"}, + "3699": {"key": 29, "change": 0.25, "bonus": 0, "name": "Prodigal Defender"}, + + "1480": {"key": 144, "change": 1.60, "bonus": 0, "name": "Hermetic Link VI"}, + "2117": {"key": 144, "change": 1.70, "bonus": 0, "name": "Mystic's Blessing"}, + "4418": {"key": 144, "change": 1.80, "bonus": 0, "name": "Incantation of Hermetic Link"}, + "3201": {"key": 144, "change": 1.05, "bonus": 1.05, "name": "Feeble Hermetic Link"}, + "3199": {"key": 144, "change": 1.10, "bonus": 1.10, "name": "Minor Hermetic Link"}, + "3202": {"key": 144, "change": 1.15, "bonus": 1.15, "name": "Moderate Hermetic Link"}, + "3200": {"key": 144, "change": 1.20, "bonus": 1.20, "name": "Major Hermetic Link"}, + "6086": {"key": 144, "change": 1.25, "bonus": 1.25, "name": "Epic Hermetic Link"}, + "6087": {"key": 144, "change": 1.30, "bonus": 1.30, "name": "Legendary Hermetic Link"} + } +} diff --git a/static/inventory.js b/static/inventory.js index 225d52e3..4660be42 100644 --- a/static/inventory.js +++ b/static/inventory.js @@ -457,8 +457,12 @@ const RESULT_COLUMNS = [ render: item => `${item.coverage ? item.coverage.replace(/,\s*/g, '
') : '-'}` }, { key: 'armor_level', label: 'Armor', sort: 'armor', defaultVisible: true, cls: 'text-right', render: item => `${item.armor_level > 0 ? item.armor_level : '-'}` }, + { key: 'base_armor_level', label: 'Base Armor', sort: 'base_armor_level', defaultVisible: false, cls: 'text-right', + render: item => `${item.base_armor_level > 0 ? item.base_armor_level : '-'}` }, { key: 'max_damage', label: 'Max Dmg', sort: 'max_damage', defaultVisible: true, cls: 'text-right', render: item => `${item.max_damage > 0 ? item.max_damage : '-'}` }, + { key: 'base_max_damage', label: 'Base Dmg', sort: 'base_max_damage', defaultVisible: false, cls: 'text-right', + render: item => `${item.base_max_damage > 0 ? item.base_max_damage : '-'}` }, { key: 'weapon_time', label: 'Speed', sort: 'weapon_time', defaultVisible: true, cls: 'text-right', render: item => `${item.weapon_time > 0 && item.weapon_time < 100 ? item.weapon_time : '-'}` }, { key: 'attack_bonus', label: 'Attack Bonus', sort: 'attack_bonus', defaultVisible: true, cls: 'text-right',