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:
parent
77e5a544d1
commit
c4856dc701
4 changed files with 141 additions and 0 deletions
|
|
@ -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'
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
63
inventory-service/spell_effects.json
Normal file
63
inventory-service/spell_effects.json
Normal file
|
|
@ -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"}
|
||||
}
|
||||
}
|
||||
|
|
@ -457,8 +457,12 @@ const RESULT_COLUMNS = [
|
|||
render: item => `<td class="text-right narrow-col">${item.coverage ? item.coverage.replace(/,\s*/g, '<br>') : '-'}</td>` },
|
||||
{ key: 'armor_level', label: 'Armor', sort: 'armor', defaultVisible: true, cls: 'text-right',
|
||||
render: item => `<td class="text-right">${item.armor_level > 0 ? item.armor_level : '-'}</td>` },
|
||||
{ key: 'base_armor_level', label: 'Base Armor', sort: 'base_armor_level', defaultVisible: false, cls: 'text-right',
|
||||
render: item => `<td class="text-right">${item.base_armor_level > 0 ? item.base_armor_level : '-'}</td>` },
|
||||
{ key: 'max_damage', label: 'Max Dmg', sort: 'max_damage', defaultVisible: true, cls: 'text-right',
|
||||
render: item => `<td class="text-right">${item.max_damage > 0 ? item.max_damage : '-'}</td>` },
|
||||
{ key: 'base_max_damage', label: 'Base Dmg', sort: 'base_max_damage', defaultVisible: false, cls: 'text-right',
|
||||
render: item => `<td class="text-right">${item.base_max_damage > 0 ? item.base_max_damage : '-'}</td>` },
|
||||
{ key: 'weapon_time', label: 'Speed', sort: 'weapon_time', defaultVisible: true, cls: 'text-right',
|
||||
render: item => `<td class="text-right">${item.weapon_time > 0 && item.weapon_time < 100 ? item.weapon_time : '-'}</td>` },
|
||||
{ key: 'attack_bonus', label: 'Attack Bonus', sort: 'attack_bonus', defaultVisible: true, cls: 'text-right',
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue