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) <noreply@anthropic.com>
This commit is contained in:
Erik 2026-04-09 12:31:39 +02:00
parent 77e5a544d1
commit c4856dc701
4 changed files with 141 additions and 0 deletions

View file

@ -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)