#!/usr/bin/env python3 """Analyze HV gear database for 2H physical build optimization.""" import json, sys 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 # Map item names to equipment slots by keyword 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): """Guess equipment slot from item name.""" 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 stats = item.get('stats',{}) # Fallback: use proportional scoring return None def is_twohand_weapon(item): """Check if item is a 2H weapon (not shield, not armor, not 1H).""" stats = item.get('stats',{}) itype = stats.get('type','') # Exclude armor types if 'Armor' in itype or 'Shield' in itype: return False # Check for 2H weapon keywords 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 # Fallback: burden >= 14 AND has attack accuracy (not burden from armor) if get_num(stats.get('burden',0)) >= 14 and 'attack accuracy' in str(stats).lower(): # Double check it's not armor with attack damage bonus if 'physical mitigation' not in str(stats).lower(): return True return False def score_weapon(item): stats = item.get('stats',{}) s = get_num(stats.get('Attack Accuracy',0)) * 3 s += get_num(stats.get('Attack Crit Damage',0)) * 80 s += get_num(stats.get('Strength',0)) * 2 s += get_num(stats.get('Parry',0)) s += get_num(stats.get('Block',0)) s += get_num(stats.get('Agility',0)) s += get_num(stats.get('Dexterity',0)) s -= get_num(stats.get('burden',0)) * 0.5 s += item.get('quality',0) * 5 return s # Filter items by usable level (within 20 levels of player level 153) def usable(item): stats = item.get('stats',{}) lv = stats.get('level','0') if lv == 'Unassigned': return True try: lv = int(lv) except: return True return lv <= 170 # allow up to L170 (within reach) 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')] slot_map = {e.get('slotType',''): e for e in equipped} print("=" * 70) print("2H PHYSICAL BUILD — GEAR ANALYSIS (Lv153, Estoc, IWBTH)") print("=" * 70) for slot_type in ['Mainhand', 'Head', 'Body', 'Hands', 'Legs', 'Feet']: current = slot_map.get(slot_type) if not current: print(f"\nšŸ›”ļø {slot_type}: No equipped item") continue print(f"\n━━━ {slot_type} ━━━") cur_stats = current.get('stats',{}) print(f" šŸ—” {current['name']} (q{current.get('quality','?')})") candidates = [] for item in armory: stats = item.get('stats',{}) if not usable(item): continue if slot_type == 'Mainhand': if not is_twohand_weapon(item): continue sfunc = score_weapon else: if stats.get('type') != 'Light Armor': continue # Match by slot type detected = detect_slot(item) if detected != slot_type: continue sfunc = score_armor candidates.append((sfunc(item), item)) candidates.sort(key=lambda x: -x[0]) for score, item in candidates[:5]: stats = item.get('stats',{}) marker = ' ⬅ EQUIPPED' if item['name'] == current['name'] else '' if slot_type == 'Mainhand': print(f" {score:7.1f} | {item['name']} ({item.get('quality','?')}){marker}") print(f" Acc:{stats.get('Attack Accuracy','?')} CritDam:{stats.get('Attack Crit Damage','?')} STR:{stats.get('Strength','?')} B:{stats.get('burden','?')}") else: print(f" {score:7.1f} | {item['name']} (q{item.get('quality','?')}) | Lv{stats.get('level','?')}{marker}") print(f" PMit:{stats.get('Physical Mitigation','?')} Evade:{stats.get('Evade','?')} STR:{stats.get('Strength','?')} B:{stats.get('burden','?')}") print("\n" + "=" * 70) print("šŸ† UPGRADE RECOMMENDATIONS") print("=" * 70) for slot_type in ['Mainhand', 'Head', 'Body', 'Hands', 'Legs', 'Feet']: current = slot_map.get(slot_type) if not current: continue candidates = [] for item in armory: stats = item.get('stats',{}) if not stats.get('type'): continue if not usable(item): continue if slot_type == 'Mainhand': if not is_twohand_weapon(item): continue else: if stats['type'] != 'Light Armor': continue if detect_slot(item) != slot_type: continue sfunc = score_weapon if slot_type == 'Mainhand' else score_armor candidates.append((sfunc(item), item)) candidates.sort(key=lambda x: -x[0]) if not candidates: continue top = candidates[0] if top[1]['name'] != current['name']: print(f"\nāŒ {slot_type}: {current['name']}") print(f"āœ… → {top[1]['name']} (q{top[1].get('quality','?')}, score {top[0]:.1f})") else: print(f"\nāœ… {slot_type}: {current['name']} — already best (score {top[0]:.1f})")