hv-unified/analyze_gear.py
GaboGG 589ed7e34c v0.15.4 - Elemental Strikes parsed + scored per wiki (Void Strike, etc.)
From EHWiki Detailed_Equip_Characteristics + Equipment_Basics:
- Elemental strikes are 'X Strike (Y%)' lines (Void/Fire/Cold/Elec/
  Wind/Holy/Dark), separate hit for ~50% of physical damage, max 2
  strikes + Void Strike
- Ethereal weapons deal Void damage (+523 Void Damage), prefixes:
  Ethereal→Void, Fiery→Fire, Arctic→Cold, Shocking→Elec,
  Tempestuous→Wind, Hallowed→Holy, Demonic→Dark
- Weapon procs by type: Piercing→Penetrated Armor (Estoc/Rapier),
  Crushing→Stun (Mace/Club), Slashing→Bleeding Wound (Longsword/
  Katana/Shortsword/Axe/Wakizashi)

Parser now captures:
- stats['Strike <Elem>'] = chance% (e.g. 'Strike Void': 50)
- stats['Weapon Damage'] for Void too (+523 Void Damage)

Scorer adds expected strike damage (chance/100 × 0.5 × weapon dmg)
weighted same as base damage. Type bonuses updated to match real
procs (Stun 1.15x > Bleeding 1.10x, Rapier 1.20x like Estoc 1.25x).

So your lottery Peerless Ethereal Rapier (+523 Void, Void Strike 50%)
now scores properly. Note: re-scrape after update for Strike fields.
2026-08-03 16:04:37 -04:00

191 lines
8.2 KiB
Python

#!/usr/bin/env python3
"""Analyze HV gear database for 2H physical build optimization.
Sources: equipped (character), armory (inventory), buy (bazaar listings).
"""
import json
def get_num(val):
if not val: return 0
s = str(val).replace('+','').replace('%','').strip()
try: return float(s)
except: return 0
def score_armor(item):
stats = item.get('stats',{})
s = get_num(stats.get('Physical Mitigation',0)) * 3
s += get_num(stats.get('Evade',0)) * 2
s += get_num(stats.get('Strength',0)) * 2
s += get_num(stats.get('Dexterity',0)) * 2
s += get_num(stats.get('Agility',0)) * 1.5
s += get_num(stats.get('Endurance',0)) * 1
s += get_num(stats.get('Crushing',0))
s += get_num(stats.get('Slashing',0))
s += get_num(stats.get('Piercing',0))
s -= get_num(stats.get('burden',0)) * 0.5
s += item.get('quality',0) * 5
return s
SLOT_KEYWORDS = {
'Head': ['helmet','cap','goggles','mask','hood','hat','circlet','crown','visor'],
'Body': ['breastplate','cuirass','robe','tunic','chest','gi','jacket','vest','hauberk'],
'Hands': ['gauntlets','gloves','mitts','bracers','handguards','fists'],
'Legs': ['leggings','greaves','pants','trousers','loincloth','kilt','breeches','chaps'],
'Feet': ['boots','sabatons','shoes','slippers','sandals'],
}
def detect_slot(item):
name = (item.get('name','') + ' ' + item.get('category','')).lower()
for slot, keywords in SLOT_KEYWORDS.items():
for kw in keywords:
if kw in name:
return slot
return None
def is_twohand_weapon(item):
stats = item.get('stats',{})
itype = stats.get('type','')
if 'Armor' in itype or 'Shield' in itype or 'Staff' in itype:
return False
name_lower = (item.get('name','') + ' ' + item.get('category','')).lower()
if any(k in name_lower for k in ['estoc','longsword','great mace','scythe','axe','club']):
return True
if get_num(stats.get('burden',0)) >= 14 and 'attack accuracy' in str(stats).lower():
if 'physical mitigation' not in str(stats).lower():
return True
return False
def score_weapon(item):
stats = item.get('stats',{})
name_lower = (item.get('name','') + ' ' + item.get('category','')).lower()
quality = item.get('quality', 0)
s = get_num(stats.get('Attack Accuracy',0)) * 2
s += get_num(stats.get('Attack Crit Damage',0)) * 60
s += get_num(stats.get('Strength',0)) * 2
s += get_num(stats.get('Dexterity',0)) * 1.5
s += get_num(stats.get('Parry',0)) * 0.5
s += get_num(stats.get('Block',0)) * 0.5
s += get_num(stats.get('Agility',0)) * 0.5
s -= get_num(stats.get('burden',0)) * 0.5
# Base weapon damage + elemental strikes
wd = get_num(stats.get('Weapon Damage',0))
s += wd * 0.30
for el in ['Fire','Cold','Elec','Wind','Holy','Dark','Void','Ethereal']:
chance = get_num(stats.get('Strike ' + el,0))
if chance > 0:
expected = (chance / 100) * 0.5 * wd
s += expected * 0.30 if expected > 0 else chance * 0.15
# Proc lines
for k, v in stats.items():
if k.startswith('Proc '):
pct = get_num(v)
pname = k.replace('Proc ','').lower()
if 'penetrated armor' in pname or 'armor' in pname: s += pct * 0.35
elif any(x in pname for x in ['stun','freeze','slow','paraly']): s += pct * 0.45
elif any(x in pname for x in ['bleeding','wound','poison','burn','shock']): s += pct * 0.25
elif 'weaken' in pname or 'impair' in pname: s += pct * 0.25
else: s += pct * 0.15
# Quality multiplier: higher tiers have better base damage
# q6=Magnificent, q5=Exquisite, q4=Superior, q3=Average, q2=Fair, q1=Crude
quality_mult = 0.8 + (quality * 0.12) # q4=1.28x, q5=1.40x, q6=1.52x
s *= quality_mult
# Weapon type bonus (wiki procs: Piercing→Penetrated Armor, Crushing→Stun,
# Slashing→Bleeding Wound; Estoc=2H Penetrated Armor is the 2H physical build's core)
if 'estoc' in name_lower:
s *= 1.25
elif 'rapier' in name_lower:
s *= 1.20 # 1H Penetrated Armor
elif any(k in name_lower for k in ['longsword','katana','shortsword','axe','wakizashi']):
s *= 1.10 # Bleeding Wound
elif any(k in name_lower for k in ['great mace','club','mace']):
s *= 1.15 # Stun
return s
def usable(item, lv=155):
stats = item.get('stats',{})
level = stats.get('level','0')
if level == 'Unassigned': return True
try: return int(level) <= lv + 15
except: return True
def lv_str(item):
stats = item.get('stats',{})
return str(stats.get('level','?'))
def src_label(item):
src = item.get('source','?')
return {'character': '🟢 Equipped', 'armory': '📦 Stored', 'buy': '🛒 For Sale'}.get(src, f'{src}')
with open('references/gear.json') as f:
gear = json.load(f)
equipped = [i for i in gear if i.get('source') == 'character' and i.get('id') and not i.get('disabled')]
armory = [i for i in gear if i.get('source') == 'armory' and i.get('stats')]
buy = [i for i in gear if i.get('source') == 'buy' and i.get('stats')]
print(f"📊 Loaded: {len(equipped)} equipped, {len(armory)} stored, {len(buy)} purchasable")
print(f"🔒 Level cap for equipping: Lv155 (player) + 15 = Lv170")
print()
slot_map = {e.get('slotType',''): e for e in equipped}
for slot_type in ['Mainhand', 'Head', 'Body', 'Hands', 'Legs', 'Feet']:
current = slot_map.get(slot_type)
print(f"\n{'='*55}")
if not current:
print(f"🛡️ {slot_type}: EMPTY")
continue
cur_stats = current.get('stats',{})
cur_lv = cur_stats.get('level','?')
print(f"🛡️ {slot_type}: {current['name']} (q{current.get('quality','?')}, Lv{cur_lv})")
print(f" ═══ CURRENTLY EQUIPPED ═══")
if slot_type == 'Mainhand':
print(f" Acc:{cur_stats.get('Attack Accuracy','?')} CritDam:{cur_stats.get('Attack Crit Damage','?')} STR:{cur_stats.get('Strength','?')} Parry:{cur_stats.get('Parry','?')} Block:{cur_stats.get('Block','?')} Burden:{cur_stats.get('burden','?')}")
else:
print(f" PMit:{cur_stats.get('Physical Mitigation','?')} Evade:{cur_stats.get('Evade','?')} STR:{cur_stats.get('Strength','?')} DEX:{cur_stats.get('Dexterity','?')} AGI:{cur_stats.get('Agility','?')} Burden:{cur_stats.get('burden','?')}")
# Show alternatives from ALL sources, sorted by score
all_items = []
for item in armory + buy:
stats = item.get('stats',{})
if not usable(item, 155): continue
if slot_type == 'Mainhand':
if not is_twohand_weapon(item): continue
sfunc = score_weapon
else:
if stats.get('type') != 'Light Armor': continue
if detect_slot(item) != slot_type: continue
sfunc = score_armor
all_items.append((sfunc(item), item))
all_items.sort(key=lambda x: -x[0])
if all_items:
print(f"\n 📊 TOP 5 ALTERNATIVES (ranked by score):")
for i, (score, item) in enumerate(all_items[:5]):
src = src_label(item)
stats = item.get('stats',{})
lv = lv_str(item)
price = item.get('price','')
price_str = f' | 💰{price}' if price else ''
marker = ' ⬅ EQUIPPED' if item['name'] == current['name'] else ''
print(f" {i+1}. [{score:6.1f}] {src} | {item['name']} (q{item.get('quality','?')}, Lv{lv}){price_str}{marker}")
if slot_type == 'Mainhand':
print(f" Acc:{stats.get('Attack Accuracy','?')} CritDam:{stats.get('Attack Crit Damage','?')} STR:{stats.get('Strength','?')}")
else:
print(f" PMit:{stats.get('Physical Mitigation','?')} Evade:{stats.get('Evade','?')} STR:{stats.get('Strength','?')} DEX:{stats.get('Dexterity','?')}")
# Check if current equipped is still best
if all_items:
best = all_items[0]
if best[1]['name'] != current['name']:
print(f"\n ❌ NOT OPTIMAL — upgrade available!")
print(f"{src_label(best[1])}: {best[1]['name']} (score {best[0]:.1f})")
if best[1].get('source') == 'buy':
print(f" 💰 Price: ${best[1].get('price','?')}")
else:
print(f"\n ✅ ALREADY BEST (score {best[0]:.1f})")