AGY built a full 60-round arena simulator (hv-battle-sim.py) using the user's actual gear/stats and ran 10 iterations: Original: 0/10 survived (0%) Improved: 10/10 survived (100%) Critical changes from simulation findings: - Health items + Cure moved to TOP of priority chain (before skills) - Dynamic HP threshold: 60% when 7+ mobs, normal threshold otherwise - SP threshold raised from 55% to 60% for Spark safety buffer - Mana maintenance raised to prevent spell lockout - AoE crowd control prioritized for 4+ mobs Full report in agy_sim_output.txt
175 lines
7.3 KiB
Python
175 lines
7.3 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
|
|
|
|
# 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: Estoc has Penetrated Armor proc (armor break)
|
|
# Longsword has Weaken proc (damage debuff on enemies)
|
|
if 'estoc' in name_lower:
|
|
s *= 1.25 # Estoc's Penetrated Armor is the defining proc for 2H physical builds
|
|
elif 'longsword' in name_lower:
|
|
s *= 1.10 # Weaken proc is decent but not as impactful
|
|
elif 'great mace' in name_lower:
|
|
s *= 0.90 # No armor pen, lower utility
|
|
elif 'club' in name_lower:
|
|
s *= 0.85
|
|
elif 'axe' in name_lower:
|
|
s *= 0.90
|
|
|
|
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})")
|