hv-unified/analyze_gear.py
GaboGG f2f296274c v0.14.17 - Faster weapon skills: lower skillOC to 60, threat to 70%, single-monster emergency
- skillOC default lowered from 75 to 60 — with 300ms Q debounce, it took
  ~6 presses to go from 50 to 70 OC (too slow against IWBTH enemies)
- Threat threshold lowered from 85% (102px) to 70% (84px) — triggers
  earlier when monsters have high SP bars
- Added single-monster emergency: if ANY enemy >90% SP, fire skills
  even without 2+ monsters (catches the lone special attacker)
- Step 0d high-threat check now fires for solitary imminent threats too
2026-07-27 20:05:47 -04:00

159 lines
6.3 KiB
Python

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