#!/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})")