Description: What Am I Looking At Item Info , an in-game bookshelf button to target items and see expanded descriptions details and explained mechanics , data is incomplete as some item properties are not accessible . currently explains enchanting materials , artifact weapons , cooking ingredients , and runes
"""
UI WAILA Item Inspect - a Razor Enhanced Python Script for Ultima Online
WAILA ( What Am I Looking At ) , display item information , formatted with extra details
a custom gump running in background as a button ( book shelf )
clicking the info bookshelf button triggers the item inspection targeter then select an item
a custom gump displays the item info and the items that can be crafted with it
** some item properties are not available through api or limited to 4 properties ( spell books dont list their multiple properties )
TODO:
- armor values update ( currently only 30% of modifier combinations known)
- add materials handling
- remove the crafting json logic ( we are adding data directly to the script currently )
- focus on core items with mechanics ( recall rune ) and weapons and armor
add the artifact weapons
** issues = item properties through api limited to 4 ? therefore the display is not displaying the full properties **
TROUBLESHOOTING:
- if "import" errors , download iron python 3.4.2 and copy the files in its "Lib" folder into your RazorEnhanced "Lib" folder
HOTKEY:: AutoStart on Login
VERSION:: 20250909
"""
import re # regex parsing the text
import os # reading the crafting json , could remove if hardcoded data
import json # maybe we conditionally load this if a crafting_recipe.json is found? that way no dependency for general use
DEBUG_MODE = False # Set to True for debugging messages
SHOW_TECHNICAL_INFO = False # Set to True to show ItemID, Hue, Serial in results
SHOW_CLOSE_BUTTON = False # hiding the close button , its cleaner , right click to close
HUE_TEXT_COLORIZED_BY_HUE = False # currently not working , because of needed conversion from hue to html color
DISPLAY = {
'show_item_graphic': True,
'show_item_id': False,
'show_item_hue': False,
'show_item_serial': False,
'show_category': False,
'show_makes': False,
'show_rarity': True,
'show_description': True,
'use_unicode_title': True,
'show_title': True,
'apply_item_hue': True,
'show_dev_text': False,
'show_crafting': False,
'show_crafting_message': False,
}
# Gump positions
LAUNCHER_X = 200
LAUNCHER_Y = 200
RESULTS_X = 200
RESULTS_Y = 250
# Separate gump IDs for the small launcher button and the results window
LAUNCHER_GUMP_ID = 0x7A11A12
RESULTS_GUMP_ID_BASE = 0x7A11A13 # base ID for cycling results gumps
RESULTS_GUMP_ID_MAX_OFFSET = 10 # cycle through 10 different IDs (0-9)
# Runtime state for cycling gump IDs
_CURRENT_GUMP_OFFSET = 0
# Map item properties to richer text for clarity
PROPERTY_REMAP = {
# Accuracy -> Tactics modifier (weapons) ā numeric-first formatting , yellow color
'accurate': '<basefont color=#5CB85C>+ 5 Tactics</basefont> <basefont color=#FFB84D>( Accurate )</basefont>',
'surpassingly accurate': '<basefont color=#5CB85C>+ 10 Tactics</basefont> <basefont color=#FFB84D>( Surpassingly Accurate )</basefont>',
'eminently accurate': '<basefont color=#5CB85C>+ 15 Tactics</basefont> <basefont color=#FFB84D>( Eminently Accurate )</basefont>',
'eminently accurately': '<basefont color=#5CB85C>+ 15 Tactics</basefont> <basefont color=#FFB84D>( Eminently Accurate )</basefont>', # common typo variant
'exceedingly accurate': '<basefont color=#5CB85C>+ 20 Tactics</basefont> <basefont color=#FFB84D>( Exceedingly Accurate )</basefont>',
'supremely accurate': '<basefont color=#5CB85C>+ 25 Tactics</basefont> <basefont color=#FFB84D>( Supremely Accurate )</basefont>',
# Damage tiers (weapons) ā numeric-first formatting , red color
'ruin': '<basefont color=#FF6B6B>+ 1 Damage</basefont> <basefont color=#FFB84D>( Ruin )</basefont>',
'might': '<basefont color=#FF6B6B>+ 3 Damage</basefont> <basefont color=#FFB84D>( Might )</basefont>',
'force': '<basefont color=#FF6B6B>+ 5 Damage</basefont> <basefont color=#FFB84D>( Force )</basefont>',
'power': '<basefont color=#FF6B6B>+ 7 Damage</basefont> <basefont color=#FFB84D>( Power )</basefont>',
'vanquishing': '<basefont color=#FF6B6B>+ 9 Damage</basefont> <basefont color=#FFB84D>( Vanquishing )</basefont>',
'exceptional': '<basefont color=#FF6B6B>+ 4 Damage</basefont> <basefont color=#FFB84D>( Exceptional )</basefont>',
# Slayer tiers ā numeric-first , orange
'lesser slaying': '<basefont color=#B084FF>+ 15%</basefont> vs type <basefont color=#FFB84D>( Lesser Slayer )</basefont>',
'slaying': '<basefont color=#B084FF>+ 20%</basefont> vs type <basefont color=#FFB84D>( Slayer )</basefont>',
'greater slaying': '<basefont color=#B084FF>+ 25%</basefont> vs type <basefont color=#FFB84D>( Greater Slayer )</basefont>',
# Durability tiers ā numeric-first formatting with light grey numbers and medium grey names
'durable': '<basefont color=#888888>+ 5 Durability</basefont> <basefont color=#AAAAAA>( Durable )</basefont>',
'substantial': '<basefont color=#888888>+ 10 Durability</basefont> <basefont color=#AAAAAA>( Substantial )</basefont>',
'massive': '<basefont color=#888888>+ 15 Durability</basefont> <basefont color=#AAAAAA>( Massive )</basefont>',
'fortified': '<basefont color=#888888>+ 20 Durability</basefont> <basefont color=#AAAAAA>( Fortified )</basefont>',
'indestructible': '<basefont color=#888888>+ 25 Durability</basefont> <basefont color=#AAAAAA>( Indestructible )</basefont>',
# Armor Rating tiers ā will be dynamically calculated with "Base + Additional AR (Modifier)" since this is unique to each item
# These are placeholders - actual values calculated in _compute_ar_modifier_text()
'defense': 'PLACEHOLDER_AR_MODIFIER',
'guarding': 'PLACEHOLDER_AR_MODIFIER',
'hardening': 'PLACEHOLDER_AR_MODIFIER',
'fortification': 'PLACEHOLDER_AR_MODIFIER',
'invulnerable': 'PLACEHOLDER_AR_MODIFIER',
# Durability tiers ā now handled dynamically per-slot in _compute_durability_text() because weapons and armor get different durability
# Armor Rating tiers ā now handled dynamically per-slot in _compute_ar_bonus_text() because each item different amount
'mastercrafted': '<basefont color=#3FA9FF>Mastercrafted</basefont>',
}
# Accuracy modifier keys to support weapon-type specific skill mapping (bows use Archery instead of Tactics)
ACCURACY_KEYS = {
'accurate',
'surpassingly accurate',
'eminently accurate',
'eminently accurately', # common typo variant
'exceedingly accurate',
'supremely accurate',
}
# Known items with custom descriptions and colored text (hue-aware via tuple keys)
# Key: (ItemID, Hue or None) -> list[str]
# Use Hue=None to apply to all hues. Add specific entries like (0x0996, 0x005F) to override for that hue only.
KNOWN_ITEMS = {
(0x1F14, None): [ # Recall Rune (all hues)
"this rune stone may store a location",
"using <basefont color=#3FA9FF>Recall</basefont> or <basefont color=#3FA9FF>GateTravel</basefont> will transport the caster to the location stored in the target rune stone",
"using <basefont color=#FF6B6B>Mark</basefont> sets the caster's location into the target rune stone",
"rename the rune stone by double clicking it",
"a <basefont color=#8B4513>RuneBook</basefont> may store multiple rune stones , by dropping them onto the book"
],
(0x0996, None): [ # Mento Seasoning (all hues) , special hue sometimes 0x005F
"a <basefont color=#228B22>Cooking</basefont> ingredient",
"used in <basefont color=#32CD32>Bowl of Marinated Rocks</basefont> (Grants 10% physical resistance for 10 min) ",
"used in <basefont color=#228B22>Colorful Salad</basefont>, <basefont color=#228B22>Pork Meal</basefont>",
"collected from <basefont color=#3FA9FF>Humanoid</basefont> enemies",
],
(0x099F, None): [ # Samuel Secret Sauce (all hues)
"a <basefont color=#228B22>Cooking</basefont> ingredient",
"used in <basefont color=#32CD32>Pixie Leg Feast</basefont> (Grants increased spell surging chance for 15 min) ",
"used in <basefont color=#32CD32>Charcuterie Board</basefont> (Grants 10% magical resistance for 10 min) ",
"used in <basefont color=#228B22>Salmon Meal</basefont>, <basefont color=#228B22>Spicy Fish Bowl</basefont> ",
"collected from <basefont color=#3FA9FF>Humanoid</basefont> enemies",
],
(0x3199, None): [ # Brilliant Amber (all hues)
"a rare gem",
"collected from <basefont color=#8B4513>Lumberjacking</basefont>",
],
(0x4FB6, None): [ # N token for raffle (all hues)
"a token for raffle",
"turn in at the <basefont color=#3FA9FF>Britain</basefont> bank top right corner",
],
}
# Enchanting materials with their specific enhancement properties (hue-aware via tuple keys)
KNOWN_ENCHANTING_ITEMS = {
(0x3197, None): [ # Fire Ruby (all hues)
"a gem for enchanting",
"used at an <basefont color=#FFB84D>Enchantment Table</basefont> for <basefont color=#FF6B6B>Weapon Damage</basefont> , and Spellbook <basefont color=#FF6B6B>Fireball</basefont>, and Armor <basefont color=#FF6B6B>Fire Elemental</basefont>",
"collected from mining and salvaging <basefont color=#3FA9FF>Magical</basefont> items ",
#imbuing "used for imbuing <basefont color=#5CB85C>Strength</basefont> properties onto armor , and <basefont color=#FFB84D>Hit Fireball</basefont> onto weapon ",
#crafting "used for crafting the <basefont color=#FF6B6B>Fiery Spellblade</basefont>",
],
(0x573C, None): [ # Arcanic Rune Stone
"an enchanting material",
"used at an <basefont color=#FFB84D>Enchantment Table</basefont> for <basefont color=#FF6B6B>Mastery Damage</basefont> (spellbook)",
"collected from <basefont color=#8B4513>Scavenging</basefont> and salvaging <basefont color=#3FA9FF>Magical</basefont> items ",
],
(0x5748, None): [ # Bottle Ichor
"an enchanting material",
"used at an <basefont color=#FFB84D>Enchantment Table</basefont> for <basefont color=#5CB85C>Weapon Life Leech</basefont>",
"collected from <basefont color=#8B4513>Scavenging</basefont> and salvaging <basefont color=#3FA9FF>Magical</basefont> items ",
],
(0x3198, None): [ # Blue Diamond
"an enchanting material",
"used at an <basefont color=#FFB84D>Enchantment Table</basefont> for <basefont color=#FFB84D>Boss Damage</basefont>",
"collected from <basefont color=#8B4513>Scavenging</basefont> and salvaging <basefont color=#3FA9FF>Magical</basefont> items ",
],
(0x5742, None): [ # Boura Pelt
"an enchanting material",
"used at an <basefont color=#FFB84D>Enchantment Table</basefont> for Armor <basefont color=#8B4513>Giant defense</basefont>, and Spellbook <basefont color=#B084FF>Earth Elemental</basefont>",
"collected from <basefont color=#8B4513>Scavenging</basefont> and salvaging <basefont color=#3FA9FF>Magical</basefont> items ",
],
(0x573B, None): [ # Crushed Glass
"an enchanting material",
"used at an <basefont color=#FFB84D>Enchantment Table</basefont> for <basefont color=#3FA9FF>Blade Spirits</basefont>",
"collected from <basefont color=#8B4513>Scavenging</basefont> and salvaging <basefont color=#3FA9FF>Magical</basefont> items ",
],
(0x5732, None): [ # Crystalline (BlackrockCrystaline)
"an enchanting material",
"used at an <basefont color=#FFB84D>Enchantment Table</basefont> for <basefont color=#FFB84D>Surging</basefont>",
"collected from <basefont color=#8B4513>Scavenging</basefont> and salvaging <basefont color=#3FA9FF>Magical</basefont> items ",
],
(0x5721, None): [ # Daemon Claw
"an enchanting material",
"used at an <basefont color=#FFB84D>Enchantment Table</basefont> for Weapon <basefont color=#FF6B6B>Savagery</basefont>, and Armor <basefont color=#8B4513>Daemonic defense</basefont>, and Spellbook <basefont color=#B084FF>Summon Daemon</basefont>",
"collected from <basefont color=#8B4513>Scavenging</basefont> and salvaging <basefont color=#3FA9FF>Magical</basefont> items ",
],
(0x26B4, None): [ # Delicate Scales
"an enchanting material",
"used at an <basefont color=#FFB84D>Enchantment Table</basefont> for <basefont color=#5CB85C>Hunter's Luck</basefont>",
"collected from <basefont color=#8B4513>Scavenging</basefont> and salvaging <basefont color=#3FA9FF>Magical</basefont> items ",
],
(0x5737, None): [ # Elven Fletching
"an enchanting material",
"used at an <basefont color=#FFB84D>Enchantment Table</basefont> for Weapon <basefont color=#3FA9FF>Weapon Accuracy</basefont>, and Spellbook <basefont color=#B084FF>Magic Arrow</basefont>",
"collected from <basefont color=#8B4513>Scavenging</basefont> and salvaging <basefont color=#3FA9FF>Magical</basefont> items ",
],
(0x2DB2, None): [ # Enchanted Essence
"an enchanting material",
"used at an <basefont color=#FFB84D>Enchantment Table</basefont> for Spellbook <basefont color=#3FA9FF>Lower Mana Cost</basefont>",
"collected from <basefont color=#8B4513>Scavenging</basefont> and salvaging <basefont color=#3FA9FF>Magical</basefont> items ",
],
(0x5745, None): [ # Faery Dust
"an enchanting material",
"used at an <basefont color=#FFB84D>Enchantment Table</basefont> for Armor <basefont color=#8B4513>Curse Resistance</basefont>, and Spellbook <basefont color=#B084FF>Mind Blast</basefont>",
"collected from <basefont color=#8B4513>Scavenging</basefont> and salvaging <basefont color=#3FA9FF>Magical</basefont> items ",
],
(0x5726, None): [ # Fey Wings
"an enchanting material",
"used at an <basefont color=#FFB84D>Enchantment Table</basefont> for Armor <basefont color=#5CB85C>Evasion</basefont>, and Spellbook <basefont color=#B084FF>Air Elemental</basefont>",
"collected from <basefont color=#8B4513>Scavenging</basefont> and salvaging <basefont color=#3FA9FF>Magical</basefont> items ",
],
(0x572C, None): [ # Goblin Blood
"an enchanting material",
"used at an <basefont color=#FFB84D>Enchantment Table</basefont> for Armor <basefont color=#8B4513>Vermin defense</basefont>",
"collected from <basefont color=#8B4513>Scavenging</basefont> and salvaging <basefont color=#3FA9FF>Magical</basefont> items ",
],
(0x572D, None): [ # Lava Serpent Crust
"an enchanting material",
"used at an <basefont color=#FFB84D>Enchantment Table</basefont> for Armor <basefont color=#8B4513>Draconic defense</basefont>, and Spellbook <basefont color=#B084FF>Flamestrike</basefont>",
"collected from <basefont color=#8B4513>Scavenging</basefont> and salvaging <basefont color=#3FA9FF>Magical</basefont> items ",
],
(0x0F87, None): [ # Lucky Coin
"a metal coin of luck used for enchanting",
"used at an <basefont color=#FFB84D>Enchantment Table</basefont> for Armor <basefont color=#5CB85C>Crafting Luck</basefont>",
"collected from <basefont color=#8B4513>Scavenging</basefont> and salvaging <basefont color=#3FA9FF>Magical</basefont> items ",
],
(0x3191, None): [ # Luminescent (FungusLuminescent)
"a glowing mushroom of luminescent fungi",
"used at an <basefont color=#FFB84D>Enchantment Table</basefont> for Spellbook <basefont color=#3FA9FF>Spell Leech</basefont>",
"collected from <basefont color=#8B4513>Scavenging</basefont> and salvaging <basefont color=#3FA9FF>Magical</basefont> items ",
#imbuing "an Imbuing ingredients to imbue the Hit Point Increase, Mana Increase and Stamina Increase property onto items.",
#crafting "used for crafting the <basefont color=#FF6B6B>Darkglow Potion</basefont>",
],
(0x2DB1, None): [ # Magical Residue
"an enchanting material",
"used at an <basefont color=#FFB84D>Enchantment Table</basefont> for Spellbook <basefont color=#3FA9FF>Lower Reagent Cost</basefont>",
"collected from <basefont color=#8B4513>Scavenging</basefont> and salvaging <basefont color=#3FA9FF>Magical</basefont> items ",
],
(0x3190, None): [ # Parasitic Plant
"an enchanting material",
"used at an <basefont color=#FFB84D>Enchantment Table</basefont> for Spellbook <basefont color=#B084FF>Summon Surge</basefont>",
"collected from <basefont color=#8B4513>Scavenging</basefont> and salvaging <basefont color=#3FA9FF>Magical</basefont> items ",
],
(0x573D, None): [ # Powdered Iron
"an enchanting material",
"used at an <basefont color=#FFB84D>Enchantment Table</basefont> for Weapon <basefont color=#FF6B6B>Attack Speed</basefont>, and Spellbook <basefont color=#B084FF>Explosion</basefont>",
"collected from <basefont color=#8B4513>Scavenging</basefont> and salvaging <basefont color=#3FA9FF>Magical</basefont> items ",
],
(0x5747, None): [ # Raptor Teeth
"an enchanting material",
"used at an <basefont color=#FFB84D>Enchantment Table</basefont> for Weapon <basefont color=#FF6B6B>Critical Damage</basefont>",
"collected from <basefont color=#8B4513>Scavenging</basefont> and salvaging <basefont color=#3FA9FF>Magical</basefont> items ",
],
(0x2DB3, None): [ # Relic Fragment
"an enchanting material",
"used at an <basefont color=#FFB84D>Enchantment Table</basefont> for Armor <basefont color=#5CB85C>Archeologist</basefont>, and Spellbook <basefont color=#B084FF>Summon Creature</basefont>",
"collected from <basefont color=#8B4513>Scavenging</basefont> and salvaging <basefont color=#3FA9FF>Magical</basefont> items ",
],
(0x5736, None): [ # Seed of Renewel
"an enchanting material",
"used at an <basefont color=#FFB84D>Enchantment Table</basefont> for Armor <basefont color=#5CB85C>Harvesting Luck</basefont>",
"collected from <basefont color=#8B4513>Scavenging</basefont> and salvaging <basefont color=#3FA9FF>Magical</basefont> items ",
],
(0x5744, None): [ # Silver Snake
"an enchanting material",
"used at an <basefont color=#FFB84D>Enchantment Table</basefont> for Spellbook <basefont color=#3FA9FF>Water Elemental</basefont>",
"collected from <basefont color=#8B4513>Scavenging</basefont> and salvaging <basefont color=#3FA9FF>Magical</basefont> items ",
],
(0x5746, None): [ # Slith Tongue
"an enchanting material",
"used at an <basefont color=#FFB84D>Enchantment Table</basefont> for Armor <basefont color=#8B4513>Poison Resistance</basefont>",
"collected from <basefont color=#8B4513>Scavenging</basefont> and salvaging <basefont color=#3FA9FF>Magical</basefont> items ",
],
(0x5720, None): [ # Spider Carapace
"an enchanting material",
"used at an <basefont color=#FFB84D>Enchantment Table</basefont> for Armor <basefont color=#8B4513>Arachnid defense</basefont>",
"collected from <basefont color=#8B4513>Scavenging</basefont> and salvaging <basefont color=#3FA9FF>Magical</basefont> items ",
],
(0x5731, None): [ # Undying Flesh
"a crafting and enchanting material",
"used at an <basefont color=#FFB84D>Enchantment Table</basefont> for Armor <basefont color=#8B4513>Undead defense</basefont>",
"used in <basefont color=#FFB84D>Tinkering</basefont> to craft the <basefont color=#CF9FFF>Dark Passage Lantern</basefont> , an important Summoner item",
"collected from <basefont color=#8B4513>Scavenging</basefont> and salvaging <basefont color=#3FA9FF>Magical</basefont> items ",
],
(0x5722, None): [ # Vial of Vitriol
"an enchanting material",
"used at an <basefont color=#FFB84D>Enchantment Table</basefont> for Weapon <basefont color=#FF6B6B>Mage Killer</basefont>, and Spellbook <basefont color=#B084FF>Harm</basefont>, and Armor <basefont color=#8B4513>Infidel Defense</basefont>",
"collected from <basefont color=#8B4513>Scavenging</basefont> and salvaging <basefont color=#3FA9FF>Magical</basefont> items ",
],
(0x573E, None): [ # Void Orb
"an enchanting material",
"used at an <basefont color=#FFB84D>Enchantment Table</basefont> for Weapon <basefont color=#B084FF>Paragon Conversion</basefont>, and Spellbook <basefont color=#B084FF>Energy Vortex</basefont>",
"collected from <basefont color=#8B4513>Scavenging</basefont> and salvaging <basefont color=#3FA9FF>Magical</basefont> items ",
],
(0x5749, None): [ # Reflective wolf eye
"an enchanting material",
"used at an <basefont color=#FFB84D>Enchantment Table</basefont> for Armor <basefont color=#B084FF>Reflective</basefont>",
"collected from <basefont color=#8B4513>Scavenging</basefont> and salvaging <basefont color=#3FA9FF>Magical</basefont> items ",
],
}
# Known items used in quest with custom descriptions and colored text and reward item id
# quest description notes = trying to be minimal here , just enough to tell where to go ( quest giver ) and why ( rewards ) without overload about specifics
# notably choosing not to include the amount needed for turn in ( could be misunderstood that you need all 15 at once )
KNOWN_QUEST_ITEMS = {
# Key: (ItemID, Hue or None) -> list[str]
(0x5737, 0x0b42): [ # Robust Harpy Feather
"a quest item",
#"a quest item for <basefont color=#3FA9FF>Sky is the Limit</basefont>",
"<basefont color=#3FA9FF>Canute</basefont> in Britain is offering the reward of <basefont color=#8B4513>Random Runic Component</basefont> or <basefont color=#FFB84D>Treasure Map</basefont> or <basefont color=#32CD32>Food Supply Crate</basefont>",
#"<basefont color=#3FA9FF>Sky is the Limit</basefont>",
],
(0x241E, None): [ # Elfic Artifact
"a quest item",
#"a quest item for <basefont color=#3FA9FF>Stolen Artifacts</basefont>",
"<basefont color=#3FA9FF>Canute</basefont> in Britain is offering the reward of <basefont color=#8B4513>Random Bulk Resource</basefont> or <basefont color=#FFB84D>3,000 Gold</basefont> or <basefont color=#CF9FFF>Random Mastery Orb</basefont>",
#"<basefont color=#3FA9FF>Stolen Artifacts</basefont>",
],
(0x1F19, 0x0829): [ # Wrong Champion Crystal
"a crystal to <basefont color=#FF6B6B>Awaken Champions</basefont>",
"place at the Altar of <basefont color=#FF6B6B>Wrong</basefont>",
],
}
KNOWN_SCROLL_ITEMS = {
# Key: (ItemID, Hue or None) -> list[str]
(0x0E34, 0x0808): [ # Spellbook Enhancement Scroll
"apply to a <basefont color=#FF6B6B>Spellbook</basefont>",
],
(0x0E34, 0x0808): [ # Weapon Enhancement Scroll
"apply to a <basefont color=#FF6B6B>Weapon</basefont>",
],
(0x0E34, 0x0808): [ # Armor Enhancement Scroll
"apply to a <basefont color=#FF6B6B>Armor</basefont>",
],
}
# Append enchanting items to the main known items dictionary
KNOWN_ITEMS.update(KNOWN_ENCHANTING_ITEMS)
KNOWN_ITEMS.update(KNOWN_QUEST_ITEMS)
KNOWN_ITEMS.update(KNOWN_SCROLL_ITEMS)
# Known artifact weapons with special descriptions (match by ItemID and exact Name)
# Format: {'item_id': int, 'name': str, 'description': str}
# Example: "Aegis Breaker" shares the same ItemID as a normal mace (0x0F5C) but includes an extra description.
KNOWN_ARTIFACT_WEAPON_ITEMS = [
# SWORDS
{ 'item_id': 0x0F5E, 'name': "Pridestalker's Blade", 'description': '<basefont color=#FFB84D>Stacking Attack Speed (3)</basefont>' }, # broadsword
{ 'item_id': 0x1441, 'name': 'Plague', 'description': '<basefont color=#5CB85C>Disease enemy</basefont>' }, # cutlass
{ 'item_id': 0x13FF, 'name': "Death's Dance", 'description': '<basefont color=#FF6B6B>Extra damage if enemy injured</basefont>' }, # katana
{ 'item_id': 0x0F61, 'name': 'Zeal', 'description': '<basefont color=#FFB84D>Stacking Attack Speed (3)</basefont>' }, # longsword
{ 'item_id': 0x13B6, 'name': 'Spectral Scimitar', 'description': '<basefont color=#FF6B6B>Ignore Armor</basefont>' }, # scimitar
{ 'item_id': 0x13B9, 'name': "Blackthorn's Blade", 'description': '<basefont color=#FF6B6B>Stacking Damage (3)</basefont>' }, # viking sword
{ 'item_id': 0x26BB, 'name': 'Breath of the Dead', 'description': '<basefont color=#FF6B6B>Damage at the cost of your own hit points</basefont>' }, # bone harvester
{ 'item_id': 0x0EC3, 'name': 'Decapitator', 'description': '<basefont color=#FF6B6B>Bleeding damage</basefont>' }, # cleaver
{ 'item_id': 0x13F6, 'name': "Butcher's Carver", 'description': '<basefont color=#FF6B6B>Leech hit points</basefont>' }, # butcher knife
{ 'item_id': 0x0EC4, 'name': 'Deviousness', 'description': '<basefont color=#FF6B6B>Leech hit points</basefont>' }, # skinning knife
# AXE TYPES
{ 'item_id': 0x0F4D, 'name': 'Demonic Embrace', 'description': '<basefont color=#FF6B6B>Burning damage</basefont>, <basefont color=#FF6B6B>extra vs burning</basefont>' }, # bardiche
{ 'item_id': 0x26BA, 'name': 'Galeforce', 'description': '<basefont color=#FF6B6B>Creates fire field under enemy</basefont>' }, # scythe
{ 'item_id': 0x0F4B, 'name': "The Twins' Rage", 'description': '<basefont color=#FFB84D>Double Attack</basefont>' }, # double axe
{ 'item_id': 0x13B0, 'name': 'Siege Breaker', 'description': '<basefont color=#FF6B6B>Shatters Armor</basefont>' }, # war axe
{ 'item_id': 0x1443, 'name': "Titan's Fall", 'description': '<basefont color=#FFB84D>Stacking Accuracy (3)</basefont>' }, # two handed axe
{ 'item_id': 0x13FB, 'name': "Giant's Will", 'description': '<basefont color=#FF6B6B>Double damage</basefont>' }, # large battle axe
{ 'item_id': 0x0F43, 'name': 'Windseeker', 'description': '<basefont color=#FFB84D>Stacking Attack Speed (3)</basefont>' }, # hatchet
{ 'item_id': 0x143E, 'name': 'Infernal Maw', 'description': '<basefont color=#FF6B6B>Creates fire field under enemy</basefont>' }, # halberd
{ 'item_id': 0x0F45, 'name': "Executioner's Calling", 'description': '<basefont color=#FF6B6B>Extra damage if enemy injured</basefont>' }, # executioner's axe
{ 'item_id': 0x0F47, 'name': 'World Splitter', 'description': '<basefont color=#FF6B6B>Stacking Damage (3)</basefont>' }, # battle axe
{ 'item_id': 0x0F49, 'name': 'Frostbite', 'description': '<basefont color=#FF6B6B>Stacking Damage (3)</basefont>' }, # axe
{ 'item_id': 0x26BD, 'name': 'Lethality', 'description': '<basefont color=#5CB85C>Lethal poison</basefont>' }, # bladed staff
{ 'item_id': 0x2D28, 'name': 'The Condemner', 'description': '<basefont color=#FF6B6B>Stacking Damage (3)</basefont>' }, # ornate axe
# FENCING
{ 'item_id': 0x1400, 'name': 'Silver Fang', 'description': '<basefont color=#5CB85C>Infect damage</basefont>' }, # kryss
{ 'item_id': 0x0F52, 'name': "Serpent's Fang", 'description': '<basefont color=#FF6B6B>Extra damage vs poisoned</basefont>' }, # dagger
{ 'item_id': 0x1405, 'name': 'Bloodthirster', 'description': '<basefont color=#FF6B6B>Leech hit points</basefont>' }, # war fork
{ 'item_id': 0x0E87, 'name': 'No Current Artifact for Pitchfork', 'description': 'PENDING' }, # pitchfork
{ 'item_id': 0x0F62, 'name': 'Mortal Reminder', 'description': '<basefont color=#3FA9FF>Stun enemy</basefont>' }, # spear
{ 'item_id': 0x26BF, 'name': 'Deathfire Grasp', 'description': '<basefont color=#FF6B6B>Summons a meteor over enemy</basefont>' }, # double bladed staff
{ 'item_id': 0x26BE, 'name': 'The Taskmaster', 'description': '<basefont color=#5CB85C>Poison surrounding enemies</basefont>' }, # pike
{ 'item_id': 0x1403, 'name': 'Corrupted Pike', 'description': '<basefont color=#B084FF>Curses enemy</basefont>' }, # short spear
# MACE & STAVES
{ 'item_id': 0x13B4, 'name': 'Umbral Shard', 'description': '<basefont color=#FF6B6B>Bleeding damage</basefont>' }, # club
{ 'item_id': 0x0F5C, 'name': 'Aegis Breaker', 'description': '<basefont color=#FF6B6B>Shatters Armor</basefont>' },# Aegis Breaker already present for mace (0x0F5C)
{ 'item_id': 0x143B, 'name': 'Harbringer', 'description': '<basefont color=#FF6B6B>Burning damage</basefont>, <basefont color=#FF6B6B>extra vs burning</basefont>' }, # maul
{ 'item_id': 0x0E86, 'name': 'No Current PickAxe Artifact', 'description': 'PENDING' },# pickaxe
{ 'item_id': 0x143D, 'name': 'The Impaler', 'description': '<basefont color=#FF6B6B>Leech hit points</basefont>' }, # hammer pick
{ 'item_id': 0x1439, 'name': 'Hellclap', 'description': '<basefont color=#FF6B6B>Burning damage</basefont>, <basefont color=#FF6B6B>extra vs burning</basefont>' }, # war hammer
{ 'item_id': 0x1407, 'name': 'Tantrum', 'description': '<basefont color=#3FA9FF>Stun enemy</basefont>' }, # war mace
{ 'item_id': 0x0E89, 'name': 'Mindcry', 'description': '<basefont color=#FFB84D>Stacking Accuracy (3)</basefont>' }, # quarter staff
{ 'item_id': 0x13F8, 'name': 'The Peacekeeper', 'description': '<basefont color=#3FA9FF>Calms surrounding creatures</basefont>' }, # gnarled staff
{ 'item_id': 0x0DF0, 'name': 'The Absorber', 'description': '<basefont color=#B084FF>Gain spell reflection</basefont>' }, # black staff
{ 'item_id': 0x0E81, 'name': 'The Shepherd', 'description': 'PENDING' }, # shepherd's crook
# ARCHERY
{ 'item_id': 0x13B2, 'name': 'Elven Bow', 'description': '<basefont color=#5CB85C>Heals friendly creatures</basefont>' }, # bow
{ 'item_id': 0x13FD, 'name': 'Widow Maker', 'description': '<basefont color=#B084FF>Shadow step</basefont>' }, # heavy crossbow
{ 'item_id': 0x0F50, 'name': 'Repugnance', 'description': '<basefont color=#FFB84D>Knockback</basefont>, <basefont color=#FF6B6B>more damage if pinned</basefont>' }, # crossbow
{ 'item_id': 0x26C2, 'name': 'The Dryad Bow', 'description': '<basefont color=#FFB84D>Distance based damage</basefont>' }, # composite bow
{ 'item_id': 0x26C3, 'name': 'Wraith Whisperer', 'description': '<basefont color=#FF6B6B>Ignore armor</basefont>' }, # repeating crossbow
{ 'item_id': 0x27A5, 'name': 'Bow of Infinite Swarms', 'description': '<basefont color=#FFB84D>Stacking Attack speed (3)</basefont>' }, # yumi
]
def resolve_artifact_weapon_description(item_id: int, item_name: str):
"""Return artifact description if this item matches a known artifact weapon by id and name.
Matching is case-insensitive on name and exact on item_id.
"""
try:
iid = int(item_id)
except Exception:
iid = item_id
nm = (item_name or '').strip()
if not nm or iid is None:
return None
low = nm.lower()
try:
for entry in (KNOWN_ARTIFACT_WEAPON_ITEMS or []):
try:
if int(entry.get('item_id')) == iid and str(entry.get('name', '')).strip().lower() == low:
return entry.get('description')
except Exception:
continue
except Exception:
pass
return None
# Colors (Razor Enhanced gump label hues)
COLORS = {
'title': 68, # blue
'label': 1153, # light gray
'ok': 63, # green
'warn': 53, # yellow
'bad': 33, # red
'cat': 90, # cyan
'info': 1153, # light gray for info messages
'success': 63, # green for success messages
}
# Define which properties are considered "modifiers" that should be displayed first
MODIFIER_PROPERTIES = {
'accurate', 'surpassingly accurate', 'eminently accurate', 'eminently accurately',
'exceedingly accurate', 'supremely accurate',
'ruin', 'might', 'force', 'power', 'vanquishing', 'exceptional',
'durable', 'substantial', 'massive', 'fortified', 'indestructible',
'defense', 'guarding', 'hardening', 'fortification', 'invulnerable',
}
MATERIAL_PROPERTIES = {
'silver', 'shadow', 'copper', 'bronze', 'golden', 'agapite', 'verite', 'valorite',
'spined', 'horned', 'barbed',
'oak', 'ash', 'yew', 'heartwood', 'bloodwood', 'frostwood'
}
# Modifier color categories
MODIFIER_COLORS = {
'durability': '#CD853F', # Light brown for durability modifiers
'damage': '#FF8C00', # Orange for damage modifiers
'skill': '#FFD700', # Yellow for skill modifiers
'default': '#CCCCCC' # Default gray for other modifiers
}
# Durability tier bonuses (fixed values for both weapons and armor)
DURABILITY_TIERS = {
'durable': 5,
'substantial': 10,
'massive': 15,
'fortified': 20,
'indestructible': 25,
}
# Armor Rating tier bonuses , these are rough estimates for fallback , the actual data is in ARMOR_DATA_BY_ITEMID
AR_BONUS_TIERS = {
'defense': {'neck_hands': 0.4, 'arms_head_legs': 0.7, 'body': 2.2, 'shield': 1, 'pct': 5},
'guarding': {'neck_hands': 0.7, 'arms_head_legs': 1.4, 'body': 4.4, 'shield': 1.5, 'pct': 10},
'hardening': {'neck_hands': 1.1, 'arms_head_legs': 2.1, 'body': 6.6, 'shield': 2, 'pct': 15},
'fortification': {'neck_hands': 1.4, 'arms_head_legs': 2.8, 'body': 8.8, 'shield': 4, 'pct': 20},
'invulnerable': {'neck_hands': 1.8, 'arms_head_legs': 3.5, 'body': 11.0, 'shield': 7, 'pct': 25},
}
# Hex colors for HTML text rendering based on tiers
NAME_TIER_COLORS = {
'common': '#DDDDDD',
'uncommon': '#5CB85C', # green
'rare': '#3FA9FF', # blue
'epic': '#B084FF', # purple
'legendary': '#FFB84D', # orange/gold
'mythic': '#FF6B6B', # red-ish
}
MATERIAL_RARITY = {
'low': {'hue': COLORS['ok']}, # green
'mid': {'hue': COLORS['warn']}, # yellow
'high': {'hue': COLORS['bad']}, # red
}
# Runtime state
_IS_TARGETING = False
_LAST_TARGET_SERIAL = None
_LAST_TARGET_ITEMID = None
_LAST_TARGET_NAME = None
_LAST_USAGES = None
#//=============================================================================
# Equipment data mapping (by ItemID)
# Includes: Type, Layer, Base AR, AR modifiers, DEX penalty
# Source: Incomplete DATA_item_armor_data_to_wiki.py results ( 137 / 432 ) items recorded ~30%
ARMOR_DATA_BY_ITEMID = {
# Comprehensive armor data from testing analysis
# Format: item_id: {'type': str, 'layer': str, 'name': str, 'base_ar': int, 'ar_modifiers': dict, 'dex_penalty': int}
# --- Platemail Armor ---
0x140C: {'type': 'Platemail', 'layer': 'Head', 'name': 'bascinet', 'base_ar': 3, 'ar_modifiers': {}, 'dex_penalty': 0},
0x1408: {'type': 'Platemail', 'layer': 'Head', 'name': 'close helmet', 'base_ar': 4, 'ar_modifiers': {'Defense': 6, 'Guarding': 7}, 'dex_penalty': 0},
0x1C04: {'type': 'Platemail', 'layer': 'InnerTorso', 'name': 'female plate', 'base_ar': 10, 'ar_modifiers': {'Defense': 16, 'Hardening': 19}, 'dex_penalty': -5},
0x140A: {'type': 'Platemail', 'layer': 'Head', 'name': 'helmet', 'base_ar': 4, 'ar_modifiers': {'Defense': 6, 'Hardening': 8}, 'dex_penalty': 0},
0x140E: {'type': 'Platemail', 'layer': 'Head', 'name': 'norse helm', 'base_ar': 4, 'ar_modifiers': {}, 'dex_penalty': 0},
0x1F0B: {'type': 'Platemail', 'layer': 'Head', 'name': 'orc helm', 'base_ar': 0, 'ar_modifiers': {'Guarding': 6}, 'dex_penalty': 0},
0x1412: {'type': 'Platemail', 'layer': 'Head', 'name': 'plate helm', 'base_ar': 4, 'ar_modifiers': {'Defense': 7, 'Fortification': 9, 'Guarding': 7}, 'dex_penalty': -1},
0x1410: {'type': 'Platemail', 'layer': 'Arms', 'name': 'platemail arms', 'base_ar': 5, 'ar_modifiers': {'Defense': 7, 'Fortification': 9, 'Guarding': 8}, 'dex_penalty': -2},
0x1413: {'type': 'Platemail', 'layer': 'Gloves', 'name': 'platemail gloves', 'base_ar': 2, 'ar_modifiers': {'Fortification': 4, 'Guarding': 4}, 'dex_penalty': -2},
0x1414: {'type': 'Platemail', 'layer': 'Neck', 'name': 'platemail gorget', 'base_ar': 2, 'ar_modifiers': {'Defense': 3}, 'dex_penalty': -1},
0x1411: {'type': 'Platemail', 'layer': 'Pants', 'name': 'platemail legs', 'base_ar': 7, 'ar_modifiers': {'Fortification': 14, 'Guarding': 11}, 'dex_penalty': -6},
0x2779: {'type': 'Platemail', 'layer': 'Neck', 'name': 'platemail mempo', 'base_ar': 0, 'ar_modifiers': {'Defense': 1}, 'dex_penalty': 0},
0x1415: {'type': 'Platemail', 'layer': 'InnerTorso', 'name': 'platemail tunic', 'base_ar': 11, 'ar_modifiers': {'Defense': 16, 'Fortification': 22, 'Guarding': 18}, 'dex_penalty': -8},
# --- Bone Armor ---
0x144F: {'type': 'Bone', 'layer': 'InnerTorso', 'name': 'bone armor', 'base_ar': 11, 'ar_modifiers': {'Defense': 16, 'Guarding': 18}, 'dex_penalty': -6},
0x144E: {'type': 'Bone', 'layer': 'Arms', 'name': 'bone arms', 'base_ar': 0, 'ar_modifiers': {'Defense': 7}, 'dex_penalty': -2},
0x1450: {'type': 'Bone', 'layer': 'Gloves', 'name': 'bone gloves', 'base_ar': 2, 'ar_modifiers': {'Guarding': 4, 'Hardening': 4}, 'dex_penalty': -1},
0x1451: {'type': 'Bone', 'layer': 'Head', 'name': 'bone helmet', 'base_ar': 0, 'ar_modifiers': {'Hardening': 8}, 'dex_penalty': 0},
0x1452: {'type': 'Bone', 'layer': 'Pants', 'name': 'bone leggings', 'base_ar': 0, 'ar_modifiers': {'Defense': 10}, 'dex_penalty': -4},
# --- Chainmail Armor ---
0x13BB: {'type': 'Chainmail', 'layer': 'Head', 'name': 'chainmail coif', 'base_ar': 4, 'ar_modifiers': {}, 'dex_penalty': 0},
0x13BE: {'type': 'Chainmail', 'layer': 'Pants', 'name': 'chainmail leggings', 'base_ar': 6, 'ar_modifiers': {}, 'dex_penalty': -3},
0x13BF: {'type': 'Chainmail', 'layer': 'InnerTorso', 'name': 'chainmail tunic', 'base_ar': 10, 'ar_modifiers': {'Defense': 15, 'Guarding': 17}, 'dex_penalty': -5},
# --- Leather Armor ---
0x1C06: {'type': 'Leather', 'layer': 'InnerTorso', 'name': 'female leather armor', 'base_ar': 5, 'ar_modifiers': {'Defense': 10, 'Fortification': 15, 'Guarding': 12}, 'dex_penalty': 0},
0x1C0A: {'type': 'Leather', 'layer': 'InnerTorso', 'name': 'leather bustier', 'base_ar': 5, 'ar_modifiers': {'Defense': 10}, 'dex_penalty': 0},
0x1DB9: {'type': 'Leather', 'layer': 'Head', 'name': 'leather cap', 'base_ar': 2, 'ar_modifiers': {}, 'dex_penalty': 0},
0x13C6: {'type': 'Leather', 'layer': 'Gloves', 'name': 'leather gloves', 'base_ar': 1, 'ar_modifiers': {'Defense': 2, 'Fortification': 3}, 'dex_penalty': 0},
0x13C7: {'type': 'Leather', 'layer': 'Neck', 'name': 'leather gorget', 'base_ar': 1, 'ar_modifiers': {'Defense': 2, 'Guarding': 2}, 'dex_penalty': 0},
0x13CB: {'type': 'Leather', 'layer': 'Pants', 'name': 'leather leggings', 'base_ar': 3, 'ar_modifiers': {'Guarding': 7}, 'dex_penalty': 0},
0x277A: {'type': 'Leather', 'layer': 'Neck', 'name': 'leather mempo', 'base_ar': 0, 'ar_modifiers': {'Hardening': 2}, 'dex_penalty': 0},
0x1C00: {'type': 'Leather', 'layer': 'Pants', 'name': 'leather shorts', 'base_ar': 3, 'ar_modifiers': {'Hardening': 8}, 'dex_penalty': 0},
0x1C08: {'type': 'Leather', 'layer': 'Pants', 'name': 'leather skirt', 'base_ar': 3, 'ar_modifiers': {'Defense': 6, 'Fortification': 9, 'Guarding': 7, 'Hardening': 8}, 'dex_penalty': 0},
0x13CD: {'type': 'Leather', 'layer': 'Arms', 'name': 'leather sleeves', 'base_ar': 2, 'ar_modifiers': {'Defense': 4, 'Fortification': 6, 'Hardening': 6, 'Invulnerable': 7}, 'dex_penalty': 0},
0x13CC: {'type': 'Leather', 'layer': 'InnerTorso', 'name': 'leather tunic', 'base_ar': 5, 'ar_modifiers': {'Defense': 10}, 'dex_penalty': 0},
# --- Ringmail Armor ---
0x13EB: {'type': 'Ringmail', 'layer': 'Gloves', 'name': 'ringmail gloves', 'base_ar': 2, 'ar_modifiers': {}, 'dex_penalty': -1},
0x13F0: {'type': 'Ringmail', 'layer': 'Pants', 'name': 'ringmail leggings', 'base_ar': 5, 'ar_modifiers': {'Defense': 8}, 'dex_penalty': -1},
0x13EE: {'type': 'Ringmail', 'layer': 'Arms', 'name': 'ringmail sleeves', 'base_ar': 0, 'ar_modifiers': {'Defense': 6, 'Guarding': 6}, 'dex_penalty': -1},
0x13EC: {'type': 'Ringmail', 'layer': 'InnerTorso', 'name': 'ringmail tunic', 'base_ar': 8, 'ar_modifiers': {}, 'dex_penalty': -2},
# --- Studded Armor ---
0x1C02: {'type': 'Studded', 'layer': 'InnerTorso', 'name': 'studded armor', 'base_ar': 6, 'ar_modifiers': {'Hardening': 14}, 'dex_penalty': 0},
0x1C0C: {'type': 'Studded', 'layer': 'InnerTorso', 'name': 'studded bustier', 'base_ar': 6, 'ar_modifiers': {'Hardening': 14}, 'dex_penalty': 0},
0x13D5: {'type': 'Studded', 'layer': 'Gloves', 'name': 'studded gloves', 'base_ar': 1, 'ar_modifiers': {'Guarding': 3}, 'dex_penalty': 0},
0x13D6: {'type': 'Studded', 'layer': 'Neck', 'name': 'studded gorget', 'base_ar': 1, 'ar_modifiers': {'Defense': 2}, 'dex_penalty': 0},
0x13DA: {'type': 'Studded', 'layer': 'Pants', 'name': 'studded leggings', 'base_ar': 4, 'ar_modifiers': {}, 'dex_penalty': 0},
0x279D: {'type': 'Studded', 'layer': 'Neck', 'name': 'studded mempo', 'base_ar': 0, 'ar_modifiers': {'Guarding': 2}, 'dex_penalty': 0},
0x13DC: {'type': 'Studded', 'layer': 'Arms', 'name': 'studded sleeves', 'base_ar': 2, 'ar_modifiers': {'Defense': 5, 'Guarding': 5}, 'dex_penalty': 0},
0x13DB: {'type': 'Studded', 'layer': 'InnerTorso', 'name': 'studded tunic', 'base_ar': 8, 'ar_modifiers': {'Defense': 11, 'Hardening': 14}, 'dex_penalty': 0},
# --- Other Armor ---
0x1718: {'type': 'Other', 'layer': 'Head', 'name': "wizard's hat", 'base_ar': 0, 'ar_modifiers': {'Defense': 5}, 'dex_penalty': 0},
# --- Shields ---
0x1BC4: {'type': 'Shield', 'layer': 'LeftHand', 'name': 'Order shield', 'base_ar': 0, 'ar_modifiers': {'Fortification': 27}, 'dex_penalty': 0},
0x1B72: {'type': 'Shield', 'layer': 'LeftHand', 'name': 'bronze shield', 'base_ar': 1, 'ar_modifiers': {'Defense': 1, 'Hardening': 1}, 'dex_penalty': 0},
0x1B73: {'type': 'Shield', 'layer': 'LeftHand', 'name': 'buckler', 'base_ar': 1, 'ar_modifiers': {}, 'dex_penalty': 0},
0x1B76: {'type': 'Shield', 'layer': 'LeftHand', 'name': 'heater shield', 'base_ar': 1, 'ar_modifiers': {}, 'dex_penalty': 0},
0x1B77: {'type': 'Shield', 'layer': 'LeftHand', 'name': 'metal kite shield', 'base_ar': 1, 'ar_modifiers': {'Hardening': 1}, 'dex_penalty': 0},
0x1B74: {'type': 'Shield', 'layer': 'LeftHand', 'name': 'metal shield', 'base_ar': 1, 'ar_modifiers': {'Defense': 1}, 'dex_penalty': 0},
0x1B78: {'type': 'Shield', 'layer': 'LeftHand', 'name': 'tear kite shield', 'base_ar': 1, 'ar_modifiers': {'Hardening': 1}, 'dex_penalty': 0},
0x1B7A: {'type': 'Shield', 'layer': 'LeftHand', 'name': 'wooden shield', 'base_ar': 1, 'ar_modifiers': {'Hardening': 1}, 'dex_penalty': 0},
}
# Weapon data dictionary (by ItemID)
# Format: item_id: {'type': str, 'hands': str, 'name': str, 'skill': str}
WEAPON_DATA_BY_ITEMID = {
# --- Axes ---
0x0F49: {'type': 'Axe', 'hands': '1h', 'name': 'axe', 'skill': 'Swordsmanship'},
0x0F47: {'type': 'Axe', 'hands': '2h', 'name': 'battle axe', 'skill': 'Swordsmanship'},
0x0F4B: {'type': 'Axe', 'hands': '2h', 'name': 'double axe', 'skill': 'Swordsmanship'},
0x0F45: {'type': 'Axe', 'hands': '2h', 'name': "executioner's axe", 'skill': 'Swordsmanship'},
0x0F43: {'type': 'Axe', 'hands': '1h', 'name': 'hatchet', 'skill': 'Swordsmanship'},
0x13FB: {'type': 'Axe', 'hands': '2h', 'name': 'large battle axe', 'skill': 'Swordsmanship'},
0x1443: {'type': 'Axe', 'hands': '2h', 'name': 'two handed axe', 'skill': 'Swordsmanship'},
0x13B0: {'type': 'Axe', 'hands': '1h', 'name': 'war axe', 'skill': 'Swordsmanship'},
# --- Swords ---
0x0F5E: {'type': 'Sword', 'hands': '1h', 'name': 'broadsword', 'skill': 'Swordsmanship'},
0x1441: {'type': 'Sword', 'hands': '1h', 'name': 'cutlass', 'skill': 'Swordsmanship'},
0x13FF: {'type': 'Sword', 'hands': '1h', 'name': 'katana', 'skill': 'Swordsmanship'},
0x0F61: {'type': 'Sword', 'hands': '1h', 'name': 'longsword', 'skill': 'Swordsmanship'},
0x13B6: {'type': 'Sword', 'hands': '1h', 'name': 'scimitar', 'skill': 'Swordsmanship'},
0x13B9: {'type': 'Sword', 'hands': '1h', 'name': 'viking sword', 'skill': 'Swordsmanship'},
# --- Maces & Staves ---
0x13B4: {'type': 'Mace', 'hands': '1h', 'name': 'club', 'skill': 'Mace Fighting'},
0x143D: {'type': 'Mace', 'hands': '1h', 'name': 'hammer pick', 'skill': 'Mace Fighting'},
0x0F5C: {'type': 'Mace', 'hands': '1h', 'name': 'mace', 'skill': 'Mace Fighting'},
0x143B: {'type': 'Mace', 'hands': '2h', 'name': 'maul', 'skill': 'Mace Fighting'},
0x1439: {'type': 'Mace', 'hands': '2h', 'name': 'war hammer', 'skill': 'Mace Fighting'},
0x1407: {'type': 'Mace', 'hands': '1h', 'name': 'war mace', 'skill': 'Mace Fighting'},
0x0DF0: {'type': 'Staff', 'hands': '2h', 'name': 'black staff', 'skill': 'Mace Fighting'},
0x13F8: {'type': 'Staff', 'hands': '2h', 'name': 'gnarled staff', 'skill': 'Mace Fighting'},
0x0E89: {'type': 'Staff', 'hands': '2h', 'name': 'quarter staff', 'skill': 'Mace Fighting'},
# --- Fencing ---
0x0EC3: {'type': 'Sword', 'hands': '1h', 'name': 'cleaver', 'skill': 'Sword'}, # originally fencing moved to swords
0x0EC4: {'type': 'Sword', 'hands': '1h', 'name': 'skinning knife', 'skill': 'Sword'}, # originally fencing moved to swords
0x13F6: {'type': 'Sword', 'hands': '1h', 'name': 'butcher knife', 'skill': 'Sword'}, # originally fencing moved to swords
0x0F52: {'type': 'Fencing', 'hands': '1h', 'name': 'dagger', 'skill': 'Fencing'},
0x0F62: {'type': 'Fencing', 'hands': '2h', 'name': 'spear', 'skill': 'Fencing'},
0x1403: {'type': 'Fencing', 'hands': '2h', 'name': 'short spear', 'skill': 'Fencing'},
0x1405: {'type': 'Fencing', 'hands': '1h', 'name': 'war fork', 'skill': 'Fencing'},
0x1401: {'type': 'Fencing', 'hands': '1h', 'name': 'kryss', 'skill': 'Fencing'},
# --- Archery ---
0x13B2: {'type': 'Bow', 'hands': '2h', 'name': 'bow', 'skill': 'Archery'},
0x13B1: {'type': 'Bow', 'hands': '2h', 'name': 'bow', 'skill': 'Archery'},
0x26C2: {'type': 'Bow', 'hands': '2h', 'name': 'composite bow', 'skill': 'Archery'},
0x0F50: {'type': 'Crossbow', 'hands': '2h', 'name': 'crossbow', 'skill': 'Archery'},
0x13FD: {'type': 'Crossbow', 'hands': '2h', 'name': 'heavy crossbow', 'skill': 'Archery'},
0x26C3: {'type': 'Crossbow', 'hands': '2h', 'name': 'repeating crossbow', 'skill': 'Archery'},
0x2D1F: {'type': 'Bow', 'hands': '2h', 'name': 'magical shortbow', 'skill': 'Archery'},
# --- Mixed/Special ---
0x0E86: {'type': 'Tool', 'hands': '1h', 'name': 'pickaxe', 'skill': 'Mining'},
0x0DF2: {'type': 'Wand', 'hands': '1h', 'name': 'magic wand', 'skill': 'Magery'},
0x26BC: {'type': 'Scepter', 'hands': '1h', 'name': 'scepter', 'skill': 'Mace Fighting'},
0x0F4D: {'type': 'Polearm', 'hands': '2h', 'name': 'bardiche', 'skill': 'Swordsmanship'},
0x143E: {'type': 'Polearm', 'hands': '2h', 'name': 'halberd', 'skill': 'Swordsmanship'},
0x26BA: {'type': 'Polearm', 'hands': '2h', 'name': 'scythe', 'skill': 'Mace Fighting'},
0x26BD: {'type': 'Staff', 'hands': '2h', 'name': 'bladed staff', 'skill': 'Swordsmanship'},
0x26BF: {'type': 'Staff', 'hands': '2h', 'name': 'double bladed staff', 'skill': 'Swordsmanship'},
0x26BE: {'type': 'Polearm', 'hands': '2h', 'name': 'pike', 'skill': 'Fencing'},
0x0E87: {'type': 'Tool', 'hands': '2h', 'name': 'pitchfork', 'skill': 'Fencing'},
0x0E81: {'type': 'Staff', 'hands': '2h', 'name': "shepherd's crook", 'skill': 'Mace Fighting'},
0x26BB: {'type': 'Polearm', 'hands': '2h', 'name': 'bone harvester', 'skill': 'Swordsmanship'},
0x26C5: {'type': 'Polearm', 'hands': '2h', 'name': 'bone harvester', 'skill': 'Swordsmanship'},
0x26C1: {'type': 'Sword', 'hands': '1h', 'name': 'crescent blade', 'skill': 'Swordsmanship'},
0x1400: {'type': 'Fencing', 'hands': '1h', 'name': 'kryss', 'skill': 'Fencing'},
0x26C0: {'type': 'Polearm', 'hands': '2h', 'name': 'lance', 'skill': 'Fencing'},
0x27A8: {'type': 'Sword', 'hands': '1h', 'name': 'bokuto', 'skill': 'Swordsmanship'},
0x27A9: {'type': 'Sword', 'hands': '2h', 'name': 'daisho', 'skill': 'Swordsmanship'},
0x27AD: {'type': 'Fencing', 'hands': '1h', 'name': 'kama', 'skill': 'Fencing'},
0x27A7: {'type': 'Fencing', 'hands': '2h', 'name': 'lajatang', 'skill': 'Fencing'},
0x27A2: {'type': 'Sword', 'hands': '2h', 'name': 'no-dachi', 'skill': 'Swordsmanship'},
0x27AE: {'type': 'Fencing', 'hands': '1h', 'name': 'nunchaku', 'skill': 'Fencing'},
0x27AF: {'type': 'Fencing', 'hands': '1h', 'name': 'sai', 'skill': 'Fencing'},
0x27AB: {'type': 'Fencing', 'hands': '1h', 'name': 'tekagi', 'skill': 'Fencing'},
0x27A3: {'type': 'Fencing', 'hands': '1h', 'name': 'tessen', 'skill': 'Fencing'},
0x27A6: {'type': 'Mace', 'hands': '2h', 'name': 'tetsubo', 'skill': 'Mace Fighting'},
0x27A4: {'type': 'Sword', 'hands': '1h', 'name': 'wakizashi', 'skill': 'Swordsmanship'},
0x27A5: {'type': 'Bow', 'hands': '2h', 'name': 'yumi', 'skill': 'Archery'},
0x2D21: {'type': 'Fencing', 'hands': '1h', 'name': 'assassin spike', 'skill': 'Fencing'},
0x2D24: {'type': 'Mace', 'hands': '1h', 'name': 'diamond mace', 'skill': 'Mace Fighting'},
0x2D1E: {'type': 'Bow', 'hands': '2h', 'name': 'elven composite longbow', 'skill': 'Archery'},
0x2D35: {'type': 'Sword', 'hands': '1h', 'name': 'elven machete', 'skill': 'Swordsmanship'},
0x2D20: {'type': 'Sword', 'hands': '1h', 'name': 'elven spellblade', 'skill': 'Swordsmanship'},
0x2D22: {'type': 'Sword', 'hands': '1h', 'name': 'leafblade', 'skill': 'Swordsmanship'},
0x2D2B: {'type': 'Bow', 'hands': '2h', 'name': 'magical shortbow', 'skill': 'Archery'},
0x2D28: {'type': 'Axe', 'hands': '2h', 'name': 'ornate axe', 'skill': 'Swordsmanship'},
0x2D33: {'type': 'Sword', 'hands': '1h', 'name': 'radiant scimitar', 'skill': 'Swordsmanship'},
0x2D32: {'type': 'Sword', 'hands': '1h', 'name': 'rune blade', 'skill': 'Swordsmanship'},
0x2D2F: {'type': 'Axe', 'hands': '2h', 'name': 'war cleaver', 'skill': 'Swordsmanship'},
0x2D25: {'type': 'Staff', 'hands': '2h', 'name': 'wild staff', 'skill': 'Mace Fighting'},
0x406B: {'type': 'Throwing', 'hands': '2h', 'name': 'soul glaive', 'skill': 'Throwing'},
0x406C: {'type': 'Throwing', 'hands': '2h', 'name': 'cyclone', 'skill': 'Throwing'},
0x4067: {'type': 'Throwing', 'hands': '2h', 'name': 'boomerang', 'skill': 'Throwing'},
0x08FE: {'type': 'Sword', 'hands': '1h', 'name': 'bloodblade', 'skill': 'Swordsmanship'},
0x0903: {'type': 'Mace', 'hands': '1h', 'name': 'disc mace', 'skill': 'Mace Fighting'},
0x090B: {'type': 'Sword', 'hands': '1h', 'name': 'dread sword', 'skill': 'Swordsmanship'},
0x0904: {'type': 'Fencing', 'hands': '2h', 'name': 'dual pointed spear', 'skill': 'Fencing'},
0x08FD: {'type': 'Axe', 'hands': '2h', 'name': 'dual short axes', 'skill': 'Swordsmanship'},
0x48B2: {'type': 'Axe', 'hands': '2h', 'name': 'gargish axe', 'skill': 'Swordsmanship'},
0x48B4: {'type': 'Polearm', 'hands': '2h', 'name': 'gargish bardiche', 'skill': 'Swordsmanship'},
0x48B0: {'type': 'Axe', 'hands': '2h', 'name': 'gargish battle axe', 'skill': 'Swordsmanship'},
0x48C6: {'type': 'Polearm', 'hands': '2h', 'name': 'gargish bone harvester', 'skill': 'Swordsmanship'},
0x48B6: {'type': 'Fencing', 'hands': '1h', 'name': 'gargish butcher knife', 'skill': 'Fencing'},
0x48AE: {'type': 'Fencing', 'hands': '1h', 'name': 'gargish cleaver', 'skill': 'Fencing'},
0x0902: {'type': 'Fencing', 'hands': '1h', 'name': 'gargish dagger', 'skill': 'Fencing'},
0x48D0: {'type': 'Sword', 'hands': '2h', 'name': 'gargish daisho', 'skill': 'Swordsmanship'},
0x48B8: {'type': 'Staff', 'hands': '2h', 'name': 'gargish gnarled staff', 'skill': 'Mace Fighting'},
0x48BA: {'type': 'Sword', 'hands': '1h', 'name': 'gargish katana', 'skill': 'Swordsmanship'},
0x48BC: {'type': 'Fencing', 'hands': '1h', 'name': 'gargish kryss', 'skill': 'Fencing'},
0x48CA: {'type': 'Polearm', 'hands': '2h', 'name': 'gargish lance', 'skill': 'Fencing'},
0x48C2: {'type': 'Mace', 'hands': '2h', 'name': 'gargish maul', 'skill': 'Mace Fighting'},
0x48C8: {'type': 'Polearm', 'hands': '2h', 'name': 'gargish pike', 'skill': 'Fencing'},
0x48C4: {'type': 'Polearm', 'hands': '2h', 'name': 'gargish scythe', 'skill': 'Mace Fighting'},
0x0908: {'type': 'Sword', 'hands': '1h', 'name': 'gargish talwar', 'skill': 'Swordsmanship'},
0x48CE: {'type': 'Fencing', 'hands': '1h', 'name': 'gargish tekagi', 'skill': 'Fencing'},
0x48CC: {'type': 'Fencing', 'hands': '1h', 'name': 'gargish tessen', 'skill': 'Fencing'},
0x48C0: {'type': 'Mace', 'hands': '2h', 'name': 'gargish war hammer', 'skill': 'Mace Fighting'},
0x0905: {'type': 'Staff', 'hands': '2h', 'name': 'glass staff', 'skill': 'Mace Fighting'},
0x090C: {'type': 'Sword', 'hands': '1h', 'name': 'glass sword', 'skill': 'Swordsmanship'},
0x0906: {'type': 'Staff', 'hands': '2h', 'name': 'serpentstone staff', 'skill': 'Mace Fighting'},
0x0907: {'type': 'Sword', 'hands': '1h', 'name': 'shortblade', 'skill': 'Swordsmanship'},
0x0900: {'type': 'Sword', 'hands': '1h', 'name': 'stone war sword', 'skill': 'Swordsmanship'},
}
# Remap common crafting material names to backpack tooltip names
# we maybe remove this , this is for crafting info
MATERIAL_NAME_REMAP = {
'flour': 'open sack of flour',
'raw ribs': 'cut of raw ribs',
'sack of flour': 'open sack of flour',
'bag of flour': 'open sack of flour',
'flour sack': 'open sack of flour',
'water': 'pitcher of water',
'water pitcher': 'pitcher of water',
'pitcher water': 'pitcher of water',
'ball of dough': 'dough',
'dough ball': 'dough',
'honey': 'jar of honey',
'jar honey': 'jar of honey',
'honey jar': 'jar of honey',
'jar of honey': 'jar of honey',
'raw fish steaks': 'raw fish steak',
}
def debug_msg(message, color=90):
if not DEBUG_MODE:
return
try:
Misc.SendMessage(f"[WALIA] {message}", color)
except Exception:
try:
print(f"[WALIA] {message}")
except Exception:
pass
def get_modifier_color_category(property_text):
"""Determine the color category for a modifier based on its content."""
text_lower = property_text.lower()
# Durability modifiers (light brown)
if any(word in text_lower for word in ['durability', 'durable', 'substantial', 'massive', 'fortified', 'indestructible']):
return 'durability'
# Damage modifiers (orange)
if any(word in text_lower for word in ['damage', 'ruin', 'might', 'force', 'power', 'vanquishing']):
return 'damage'
# Skill modifiers (yellow) - includes tactics, skills, and other stat bonuses
if any(word in text_lower for word in ['tactics', 'skill', 'accurate', 'anatomy', 'archery', 'fencing', 'mace', 'swords', 'wrestling']):
return 'skill'
# Default for other modifiers
return 'default'
def format_modifier_text(property_text):
"""Format modifier text with appropriate colors and extract numeric values."""
color_category = get_modifier_color_category(property_text)
color = MODIFIER_COLORS[color_category]
# Extract numeric values and format them prominently
import re
# Look for patterns like "+5", "+ 10", "20", etc.
number_match = re.search(r'[+\-]?\s*(\d+)', property_text)
if number_match:
number = number_match.group(1)
# Replace the number in the text with colored version
formatted_text = re.sub(
r'([+\-]?\s*)(\d+)',
f'<basefont color={color}>\\1{number}</basefont>',
property_text,
count=1
)
return formatted_text
else:
# No number found, just color the whole text
return f'<basefont color={color}>{property_text}</basefont>'
def get_armor_data(item_id):
"""Return armor data for an item id, or None if unknown."""
try:
return ARMOR_DATA_BY_ITEMID.get(int(item_id))
except Exception:
return ARMOR_DATA_BY_ITEMID.get(item_id)
def get_weapon_data(item_id):
"""Return weapon data for an item id, or None if unknown."""
try:
return WEAPON_DATA_BY_ITEMID.get(int(item_id))
except Exception:
return WEAPON_DATA_BY_ITEMID.get(item_id)
def get_equip_slot(item_id):
"""Return equip layer for an item id, or None if unknown."""
armor_data = get_armor_data(item_id)
if armor_data:
return armor_data.get('layer')
return None
def is_weapon(item_id):
"""Check if an item is a weapon."""
return get_weapon_data(item_id) is not None
def is_armor(item_id):
"""Check if an item is armor or shield."""
return get_armor_data(item_id) is not None
def get_weapon_abilities(item_id):
"""Get weapon abilities for an item using Razor Enhanced API.
Returns tuple of (primary_ability, secondary_ability) or (None, None) if not a weapon.
"""
try:
abilities = Items.GetWeaponAbility(int(item_id))
if abilities:
# Handle ValueTuple[str, str] return type
primary, secondary = abilities.Item1, abilities.Item2
# Filter out "Invalid" responses
if primary == "Invalid":
primary = None
if secondary == "Invalid":
secondary = None
return primary, secondary
except Exception as e:
debug_msg(f"Error getting weapon abilities for {item_id}: {e}", COLORS['warn'])
return None, None
def get_item_properties(item_serial, delay=500):
"""Get detailed item properties using Items.GetProperties() API.
Returns list of Property objects with more comprehensive information.
"""
try:
properties = Items.GetProperties(int(item_serial), int(delay))
if properties:
return list(properties)
except Exception as e:
debug_msg(f"Error getting properties for serial {item_serial}: {e}", COLORS['warn'])
return []
def _resolve_known_item_lines(item_id: int, item_hue: int, treat_hue_as_agnostic: bool = False) -> list:
"""Resolve known item description lines using the new tuple-key system only.
Priority:
1) If treat_hue_as_agnostic: (item_id, None)
2) Else: if hue in (0, None) -> (item_id, None)
3) Else: (item_id, hue) -> (item_id, None)
4) Scan fallback over tuple keys for this item_id
"""
# 1/2. Tuple-key lookups
try:
iid = int(item_id)
except Exception:
iid = item_id
# Use flexible int/hex conversion for hue
hue = _to_int_id(item_hue)
if hue is None:
try:
hue = int(item_hue)
except Exception:
hue = 0
# When hue is agnostic (weapons/armor), skip hue-specific
if treat_hue_as_agnostic:
if (iid, None) in KNOWN_ITEMS and isinstance(KNOWN_ITEMS[(iid, None)], list):
return KNOWN_ITEMS[(iid, None)]
else:
# If hue is effectively "no hue" (0/None), prefer the generic (iid, None)
if hue in (0, None):
if (iid, None) in KNOWN_ITEMS and isinstance(KNOWN_ITEMS[(iid, None)], list):
return KNOWN_ITEMS[(iid, None)]
# Otherwise attempt exact hue match first, then generic
if (iid, hue) in KNOWN_ITEMS and isinstance(KNOWN_ITEMS[(iid, hue)], list):
return KNOWN_ITEMS[(iid, hue)]
if (iid, None) in KNOWN_ITEMS and isinstance(KNOWN_ITEMS[(iid, None)], list):
return KNOWN_ITEMS[(iid, None)]
# Scan fallback: find any tuple key matching iid, prefer None hue
try:
for k, v in KNOWN_ITEMS.items():
if isinstance(k, tuple) and len(k) == 2 and k[0] == iid and isinstance(v, list):
# Prefer None hue explicitly
if k[1] is None:
return v
# If none had None hue, return the first matching iid tuple
for k, v in KNOWN_ITEMS.items():
if isinstance(k, tuple) and len(k) == 2 and k[0] == iid and isinstance(v, list):
return v
except Exception:
pass
# Nothing found
return []
def get_next_results_gump_id():
"""Get the next cycling RESULTS_GUMP_ID and increment the counter."""
global _CURRENT_GUMP_OFFSET
current_id = RESULTS_GUMP_ID_BASE + _CURRENT_GUMP_OFFSET
_CURRENT_GUMP_OFFSET = (_CURRENT_GUMP_OFFSET + 1) % RESULTS_GUMP_ID_MAX_OFFSET
return current_id
def _singularize(word: str) -> str:
if len(word) > 3 and word.endswith('s'):
return word[:-1]
return word
def name_to_fuzzy_key(name: str) -> str:
try:
n = (name or '').strip().lower()
if not n or n == 'unknown':
return ''
n = re.sub(r"[^a-z0-9\s]", " ", n)
toks = [t for t in n.split() if t]
out = []
for t in toks:
if t.isdigit():
continue
if t not in ('raw','cooked'):
t = _singularize(t)
out.append(t)
if not out:
return ''
out.sort()
return ' '.join(out)
except Exception:
return ''
def _script_root_paths():
here = os.path.abspath(os.path.dirname(__file__))
project_root = os.path.abspath(os.path.join(here, os.pardir))
data_dir = os.path.join(project_root, 'data')
return project_root, data_dir
def _find_latest_crafting_json(data_dir: str) -> str:
if not os.path.isdir(data_dir):
return None
candidates = []
for name in os.listdir(data_dir):
if not name.lower().endswith('.json'):
continue
if not name.lower().startswith('gump_crafting'):
continue
full = os.path.join(data_dir, name)
try:
mtime = os.path.getmtime(full)
except Exception:
continue
candidates.append((mtime, full))
if not candidates:
return None
candidates.sort(key=lambda x: x[0], reverse=True)
return candidates[0][1]
def _read_json(path: str):
with open(path, 'r', encoding='utf-8') as f:
return json.load(f)
def _normalize_items(json_root):
# Case 1: already a list of items
if isinstance(json_root, list):
return json_root
# Case 2: structured by categories
items = []
cats = (json_root or {}).get('categories', {})
for cat_key, cat_data in cats.items():
for it in (cat_data.get('items') or []):
parsed = (it or {}).get('parsed') or {}
if parsed:
if 'category' not in parsed:
parsed['category'] = cat_key
# carry button ids if present
if 'button_info' in it and 'item_info_button_id' not in parsed:
parsed['item_info_button_id'] = it.get('button_info')
if 'button_make' in it and 'item_make_button_id' not in parsed:
parsed['item_make_button_id'] = it.get('button_make')
items.append(parsed)
return items
def _to_int_id(v):
try:
if v is None:
return None
if isinstance(v, int):
return v
s = str(v).strip()
if s.lower().startswith('0x'):
return int(s, 16)
return int(s)
except Exception:
return None
def _normalize_material_name(nm: str) -> str:
n = (nm or '').strip().lower()
if not n:
return n
return MATERIAL_NAME_REMAP.get(n, n)
def build_material_index(items: list) -> dict:
"""Build index mapping material identifiers to recipes.
Returns dict with:
- by_id: {int item_id -> [recipe dicts]}
- by_name: {normalized_name -> [recipe dicts]}
- by_fuzzy: {fuzzy_key -> [recipe dicts]}
"""
idx = {
'by_id': {},
'by_name': {},
'by_fuzzy': {},
}
for rec in (items or []):
mats = rec.get('materials') or []
for m in mats:
# id-based
mid = _to_int_id(m.get('id') if m.get('id') is not None else m.get('id_hex'))
if isinstance(mid, int):
idx['by_id'].setdefault(int(mid), []).append(rec)
# name-based
nm = _normalize_material_name(m.get('name'))
if nm:
idx['by_name'].setdefault(nm, []).append(rec)
fk = name_to_fuzzy_key(nm)
if fk:
idx['by_fuzzy'].setdefault(fk, []).append(rec)
return idx
# Razor Enhanced helpers -----------------------------
def _pause(ms):
Misc.Pause(int(ms))
def _clean_leading_amount(name: str, amount: int) -> str:
try:
amt = int(amount)
except Exception:
return name
if not name:
return name
if amt > 1:
pattern = r'^\s*(?:\(|\[)?\s*' + re.escape(str(amt)) + r'\s*(?:\)|\])?\s*(?:x|Ć|X)?\s*[:\-*]?\s*'
cleaned = re.sub(pattern, '', name).strip()
if cleaned != name:
return cleaned
generic = re.sub(r'^\s*(?:\(|\[)?\s*\d+\s*(?:\)|\])?\s*(?:x|Ć|X)?\s*[:\-*]?\s*', '', name).strip()
return generic
def get_item_name(item, amount_hint=None):
try:
nm = item.Name
if nm:
nm = str(nm)
amt = amount_hint if amount_hint is not None else item.Amount
return _clean_leading_amount(nm, amt)
except Exception:
pass
try:
Items.WaitForProps(item.Serial, 400)
props = Items.GetPropStringList(item.Serial)
if props:
nm = str(props[0])
amt = amount_hint if amount_hint is not None else item.Amount
return _clean_leading_amount(nm, amt)
Items.SingleClick(item.Serial)
_pause(150)
Items.WaitForProps(item.Serial, 600)
props = Items.GetPropStringList(item.Serial)
if props:
nm = str(props[0])
amt = amount_hint if amount_hint is not None else item.Amount
return _clean_leading_amount(nm, amt)
except Exception:
pass
return 'Unknown'
def _fmt_hex4(v: int) -> str:
try:
return f"0x{int(v)&0xFFFF:04X}"
except Exception:
return "0x0000"
# WALIA core -----------------------------
def _rarity_for_recipe(rec: dict) -> str:
# Simple heuristic: use skill_required or material count
try:
sr = float(rec.get('skill_required') or 0)
except Exception:
sr = 0.0
mats = rec.get('materials') or []
if sr >= 90 or len(mats) >= 5:
return 'high'
if sr >= 70 or len(mats) >= 3:
return 'mid'
return 'low'
def find_usages_for_item(item, index: dict) -> list:
"""Return list of recipes that use the targeted item as a material.
Matches by itemID, normalized name, or fuzzy name.
"""
matches = []
try:
gid = int(item.ItemID)
except Exception:
gid = None
name = get_item_name(item, amount_hint=getattr(item, 'Amount', 1))
n_norm = _normalize_material_name(name)
fkey = name_to_fuzzy_key(n_norm)
seen = set()
# by id
if isinstance(gid, int) and gid in index.get('by_id', {}):
for r in index['by_id'][gid]:
k = (r.get('category'), r.get('name'))
if k not in seen:
matches.append(r)
seen.add(k)
# by normalized name
if n_norm and n_norm in index.get('by_name', {}):
for r in index['by_name'][n_norm]:
k = (r.get('category'), r.get('name'))
if k not in seen:
matches.append(r)
seen.add(k)
# by fuzzy
if fkey and fkey in index.get('by_fuzzy', {}):
for r in index['by_fuzzy'][fkey]:
k = (r.get('category'), r.get('name'))
if k not in seen:
matches.append(r)
seen.add(k)
return matches
def _build_item_description(name: str, usages: list) -> str:
try:
total = len(usages or [])
if total <= 0:
return "No known crafting usages found."
cats = sorted({(u.get('category') or 'Unknown') for u in usages})
cats_txt = ', '.join(cats[:6]) + ("ā¦" if len(cats) > 6 else "")
return f"Used in {total} recipes across: {cats_txt}"
except Exception:
return ""
def _stylize_unicode(text: str, style: str = 'fullwidth') -> str:
"""Return a unicode-styled variant of ASCII text.
Styles:
- 'fullwidth': convert ASCII to fullwidth and convert space to fullwidth space (\u3000).
- 'fullwidth_nospace': convert ASCII to fullwidth but keep regular ASCII space ' '.
"""
if not text:
return text
if style in ('fullwidth', 'fullwidth_nospace'):
out = []
for ch in text:
o = ord(ch)
if 0x21 <= o <= 0x7E: # visible ASCII
out.append(chr(o - 0x21 + 0xFF01))
elif ch == ' ':
if style == 'fullwidth':
out.append('\u3000') # fullwidth space
else:
out.append(' ') # keep normal space
else:
out.append(ch)
return ''.join(out)
return text
def _derive_name_color(prop_list: list) -> str:
"""Infer item Name color from properties. Looks for 'Magical Intensity X (Tier)'.
Returns an HTML hex color string or default white when unknown.
"""
try:
if not prop_list:
return '#FFFFFF'
import re as _re
for ln in prop_list:
s = str(ln).strip()
m = _re.search(r"magical\s+intensity\s*\d+\s*\(([^)]+)\)", s, flags=_re.IGNORECASE)
if m:
tier = m.group(1).strip().lower()
# Normalize common tier names
if tier in NAME_TIER_COLORS:
return NAME_TIER_COLORS[tier]
# Map aliases
alias = {
'epic': 'epic', 'legend': 'legendary', 'legendary': 'legendary',
'mythic': 'mythic', 'rare': 'rare', 'uncommon': 'uncommon', 'common': 'common'
}.get(tier)
if alias and alias in NAME_TIER_COLORS:
return NAME_TIER_COLORS[alias]
return '#FFFFFF'
except Exception:
pass
return '#FFFFFF'
# Modular text section rendering system
class TextSection:
"""Represents a section of text with multiple lines and formatting."""
def __init__(self, lines: list, category: str, priority: int = 100, separator_before: bool = False):
self.lines = lines # List of raw HTML strings (preserves existing formatting)
self.category = category
self.priority = priority # Lower numbers = higher priority (displayed first)
self.separator_before = separator_before
def to_html(self) -> str:
"""Convert to HTML with line breaks, ensuring each line has proper color formatting."""
if not self.lines:
return ""
# Join with <br> but ensure each line maintains its formatting
formatted_lines = []
for line in self.lines:
# If line doesn't have basefont color, it will inherit from parent or default to black
# Make sure each line is properly wrapped
if line.strip():
formatted_lines.append(line)
return "<br>".join(formatted_lines)
def height_estimate(self) -> int:
"""Estimate height in pixels for this section."""
if not self.lines:
return 0
base_height = len(self.lines) * 18 # 18px per line
separator_height = 18 if self.separator_before else 0
return base_height + separator_height
def _property_color_for_line(raw_lower: str) -> str:
"""Return HTML hex color for a given property line (lowercased)."""
try:
if not raw_lower:
return '#888888' # Medium grey for regular properties
if raw_lower.startswith('durability'):
return '#888888' # Medium grey for durability
# Add more rules as needed
except Exception:
pass
return '#888888' # Medium grey for all regular properties
def _wrap_line_with_default_color(line: str, default_color: str = '#BBBBBB') -> str:
"""Wrap a line with default color, preserving existing basefont tags."""
if not line.strip():
return line
# If line has no basefont tags, just wrap it
if '<basefont' not in line:
return f"<basefont color={default_color}>{line}</basefont>"
# Split on basefont tags to inject default color around them
import re
# Find all basefont color tags and their positions
basefont_pattern = r'<basefont\s+color=[^>]*>'
end_pattern = r'</basefont>'
result = f"<basefont color={default_color}>"
last_pos = 0
# Find all basefont start tags
for match in re.finditer(basefont_pattern, line):
# Add text before this basefont tag with default color
if match.start() > last_pos:
text_before = line[last_pos:match.start()]
if text_before.strip():
result += text_before
# Close default color before custom color
result += f"</basefont>{match.group()}"
last_pos = match.end()
# Find the corresponding end tag
remaining_text = line[last_pos:]
end_match = re.search(end_pattern, remaining_text)
if end_match:
# Add the colored text
colored_text = remaining_text[:end_match.end()]
result += colored_text
last_pos += end_match.end()
# Restart default color after custom color
result += f"<basefont color={default_color}>"
# Add any remaining text
if last_pos < len(line):
remaining = line[last_pos:]
if remaining.strip():
result += remaining
result += "</basefont>"
return result
def _estimate_text_width(text: str, char_width: int = 7) -> int:
"""Estimate text width in pixels, ignoring HTML tags."""
import re
# Remove HTML tags for width calculation
clean_text = re.sub(r'<[^>]+>', '', text)
return len(clean_text) * char_width
def _split_line_for_wrapping(line: str, max_chars: int = 30) -> list:
"""Split a long line into multiple lines based on character count and word boundaries."""
import re
# Remove HTML tags to get clean text for length calculation
clean_text = re.sub(r'<[^>]+>', '', line)
# If line is short enough, return as-is
if len(clean_text) <= max_chars:
return [line]
# For lines with HTML formatting, we need HTML-aware splitting
if '<basefont' in line:
return _split_html_line(line, max_chars)
# Simple word-based wrapping for plain text
words = line.split()
lines = []
current_line = ""
for word in words:
test_line = f"{current_line} {word}".strip()
if len(test_line) <= max_chars:
current_line = test_line
else:
if current_line:
lines.append(current_line)
current_line = word
# Handle very long single words
if len(word) > max_chars:
lines.append(current_line)
current_line = ""
if current_line:
lines.append(current_line)
return lines if lines else [line]
def _split_html_line(line: str, max_chars: int = 30) -> list:
"""Split HTML-formatted line while preserving color formatting."""
import re
# Extract text segments and their formatting
segments = []
basefont_pattern = r'<basefont\s+color=([^>]*)>(.*?)</basefont>'
last_pos = 0
# Find all basefont segments
for match in re.finditer(basefont_pattern, line):
# Add any text before this basefont
if match.start() > last_pos:
prefix_text = line[last_pos:match.start()].strip()
if prefix_text:
segments.append(('default', prefix_text))
# Add the colored segment
color = match.group(1)
text = match.group(2)
segments.append((color, text))
last_pos = match.end()
# Add any remaining text
if last_pos < len(line):
suffix_text = line[last_pos:].strip()
if suffix_text:
segments.append(('default', suffix_text))
# If no HTML found, treat as plain text
if not segments:
segments = [('default', line)]
# Now split segments into lines based on character count
lines = []
current_line_segments = []
current_char_count = 0
for color, text in segments:
words = text.split()
for word in words:
word_len = len(word)
# Check if adding this word would exceed limit
space_needed = 1 if current_char_count > 0 else 0
if current_char_count + space_needed + word_len > max_chars and current_line_segments:
# Finish current line
lines.append(_rebuild_html_line(current_line_segments))
current_line_segments = []
current_char_count = 0
# Add word to current line
current_line_segments.append((color, word))
current_char_count += word_len + (1 if current_char_count > 0 else 0)
# Add final line if any segments remain
if current_line_segments:
lines.append(_rebuild_html_line(current_line_segments))
return lines if lines else [line]
def _rebuild_html_line(segments: list) -> str:
"""Rebuild HTML line from color/text segments."""
if not segments:
return ""
result = "<basefont color=#BBBBBB>" # Start with default color
current_color = '#BBBBBB'
for i, (color, word) in enumerate(segments):
# Add space before word (except first)
if i > 0:
result += " "
# Change color if needed
if color != 'default' and color != current_color:
result += f"</basefont><basefont color={color}>{word}</basefont><basefont color=#BBBBBB>"
current_color = '#BBBBBB'
else:
result += word
result += "</basefont>"
return result
def _compute_ar_bonus_text(equip_slot: str, raw_lower: str) -> str or None:
"""Given an equip slot and a property line (lowercased), return a slot-specific AR bonus text.
Returns None when not an AR tier line.
"""
# Slot-aware AR bonus computation for armor rating tiers
if not equip_slot or not raw_lower:
return None
# Identify AR tier keyword
found = None
for key in AR_BONUS_TIERS.keys():
if key in raw_lower:
found = key
break
if not found:
return None
t = AR_BONUS_TIERS[found]
# Map slot to appropriate bucket
slot = (equip_slot or '').lower()
if slot in ('neck', 'hand'):
delta = t['neck_hands']
elif slot in ('arms', 'head', 'legs'):
delta = t['arms_head_legs']
elif slot == 'body':
delta = t['body']
elif slot == 'shield':
delta = t['shield']
else:
# Non-armor/shield slots: do not display AR percent; suppress line
return None
# Numeric-first formatting
return f"+ {delta:g} AR ( {found.capitalize()} )"
def _is_weapon_slot(equip_slot: str) -> bool:
slot = (equip_slot or '').lower()
return slot in ('weapon1h', 'weapon2h')
def _compute_durability_text(equip_slot: str, raw_lower: str) -> str or None:
"""Return durability text tailored for armor vs weapons. None if not a durability tier line."""
if not raw_lower:
return None
# Exceptional is percent-based per spec
if 'exceptional' == raw_lower.strip():
if _is_weapon_slot(equip_slot):
return '+ 20% durability ( Exceptional )'
else:
return '+ 20% durability ( Exceptional )'
for k, val in DURABILITY_TIERS.items():
if k in raw_lower:
if _is_weapon_slot(equip_slot):
return f"+ {val} durability ( {k.capitalize()} )"
else:
return f"+ {val} durability ( {k.capitalize()} )"
return None
def _compute_ar_modifier_text(item_id: int, modifier_name: str) -> str:
"""Compute AR modifier text in 'Base + Additional AR (Modifier)' format."""
if item_id not in ARMOR_DATA_BY_ITEMID:
return f"<basefont color=#888888>+ AR</basefont> <basefont color=#AAAAAA>( {modifier_name.capitalize()} )</basefont>"
armor_data = ARMOR_DATA_BY_ITEMID[item_id]
base_ar = armor_data.get('base_ar', 0)
ar_modifiers = armor_data.get('ar_modifiers', {})
# Find the total AR for this specific modifier (stored value is total, not bonus)
modifier_total_ar = ar_modifiers.get(modifier_name.capitalize(), 0)
if modifier_total_ar > 0:
# Calculate the actual bonus by subtracting base AR from total AR
modifier_bonus = modifier_total_ar - base_ar
return f"<basefont color=#3FA9FF>{base_ar} + {modifier_bonus} AR</basefont> <basefont color=#AAAAAA>( {modifier_name.capitalize()} )</basefont>"
else:
return f"<basefont color=#888888>+ AR</basefont> <basefont color=#AAAAAA>( {modifier_name.capitalize()} )</basefont>"
def _equip_slot_and_type(item_id: int) -> tuple:
"""Return (slot, friendly_type). friendly_type includes detailed armor/weapon info."""
try:
slot = get_equip_slot(int(item_id) if item_id is not None else 0)
except Exception:
slot = None
# Check armor data first for detailed info
if item_id in ARMOR_DATA_BY_ITEMID:
armor_data = ARMOR_DATA_BY_ITEMID[item_id]
armor_type = armor_data['type']
layer = armor_data['layer']
if armor_type == 'Shield':
typ = f"Shield ({layer})"
else:
typ = f"{armor_type} ( {layer} )"
return slot, typ
# Check weapon data
if item_id in WEAPON_DATA_BY_ITEMID:
weapon_data = WEAPON_DATA_BY_ITEMID[item_id]
weapon_type = weapon_data['type']
hands = weapon_data['hands']
typ = f"{weapon_type} ({hands.upper()})"
return slot, typ
# Fallback to generic slot-based detection
s = (slot or '').lower()
if s in ('weapon1h', 'weapon2h'):
typ = 'Weapon' + (' (2H)' if s == 'weapon2h' else ' (1H)')
elif s == 'shield':
typ = 'Shield'
elif s in ('head','neck','body','legs','arms','hand'):
typ = 'Armor'
else:
typ = 'Unknown'
return slot, typ
def build_text_sections(target_item, usages: list) -> list:
"""Build list of TextSection objects for modular gump content."""
sections = []
# Get basic item info
item_display_name = get_item_name(target_item, amount_hint=getattr(target_item, 'Amount', 1))
item_id = int(getattr(target_item, 'ItemID', 0) or 0)
equip_slot, friendly_type = _equip_slot_and_type(item_id)
# 1. Item properties section (highest priority) - separated into modifiers, regular properties, and durability status
modifier_lines = []
regular_lines = []
durability_lines = []
try:
Items.WaitForProps(getattr(target_item, 'Serial', 0), 400)
property_list = Items.GetPropStringList(getattr(target_item, 'Serial', 0)) or []
debug_msg(f"RAW PROPERTIES: Found {len(property_list)} properties", COLORS['cat'])
for i, prop in enumerate(property_list):
debug_msg(f" [{i}] {repr(prop)}", COLORS['cat'])
# Skip name line if duplicates title
if property_list and property_list[0].strip().lower() == (item_display_name or '').strip().lower():
property_list = property_list[1:]
debug_msg(f"SKIPPED duplicate name line, {len(property_list)} properties remaining", COLORS['cat'])
# Check if this is a weapon for special processing
is_weapon = item_id in WEAPON_DATA_BY_ITEMID
weapon_damage_text = None
weapon_speed_text = None
weapon_skill_text = None
weapon_intensity_text = None
hue_text = None
# Process each property with slot-aware rendering and separate modifiers from regular properties
debug_msg(f"PROCESSING {len(property_list[:12])} properties (limited to 12), is_weapon={is_weapon}", COLORS['cat'])
for prop_idx, prop in enumerate(property_list[:12]): # Limit to prevent overflow
try:
raw_line = str(prop).strip()
low = raw_line.lower()
debug_msg("\n--- PROPERTY {}: {} ---".format(prop_idx+1, repr(raw_line)), COLORS['cat'])
# Check if this is a durability status line (e.g., "durability 46 / 51")
import re
is_durability_status = re.match(r'durability\s+\d+\s*/\s*\d+', low)
# Check for hue property (e.g., "Metallic (#2311)" or "Inferno (#2136)")
hue_match = re.match(r'(.+?)\s*\(#(\d+)\)', raw_line)
if hue_match:
hue_name, hue_number = hue_match.groups()
hue_number = int(hue_number)
# Get item's actual hue for verification
item_hue = int(getattr(target_item, 'Hue', 0) or 0)
debug_msg(f" ā HUE PROPERTY: name='{hue_name}', number={hue_number}, item_hue={item_hue}", COLORS['success'])
# Verify hue matches (allow for some tolerance in case of conversion differences)
if abs(hue_number - item_hue) <= 1:
if HUE_TEXT_COLORIZED_BY_HUE and item_hue > 0:
# Convert hue to hex color (simplified conversion)
hue_hex = f"#{item_hue:04X}FF" # Add alpha for visibility
hue_text = f"<basefont color={hue_hex}>Hue: {hue_name.strip()} ( {hue_number} )</basefont>"
else:
hue_text = f"<basefont color=#444444>Hue: {hue_name.strip()} ( {hue_number} )</basefont>"
debug_msg(f" ā HUE TEXT: {hue_text}", COLORS['success'])
continue
else:
debug_msg(f" ā HUE MISMATCH: property={hue_number}, item={item_hue}", COLORS['warn'])
# Special weapon property handling
if is_weapon:
# Extract weapon damage (e.g., "weapon damage 8 - 32")
damage_match = re.match(r'weapon damage (\d+) - (\d+)', low)
if damage_match:
min_dmg, max_dmg = damage_match.groups()
weapon_damage_text = f"<basefont color=#FF6B6B>{min_dmg} - {max_dmg} damage</basefont>"
debug_msg(f" ā WEAPON DAMAGE: {weapon_damage_text}", COLORS['success'])
continue
# Extract weapon speed (e.g., "weapon speed 30")
speed_match = re.match(r'weapon speed (\d+)', low)
if speed_match:
speed = speed_match.group(1)
weapon_speed_text = f"<basefont color=#FFB84D>{speed} speed</basefont>"
debug_msg(f" ā WEAPON SPEED: {weapon_speed_text}", COLORS['success'])
continue
# Extract skill required (e.g., "skill required: mace fighting")
skill_match = re.match(r'skill required: (.+)', low)
if skill_match:
skill = skill_match.group(1)
weapon_skill_text = f"<basefont color=#666666>Skill: {skill.title()}</basefont>"
debug_msg(f" ā WEAPON SKILL: {weapon_skill_text}", COLORS['success'])
continue
# Extract magical intensity (e.g., "Magical Intensity: 0 (Common)")
intensity_match = re.match(r'magical intensity: \d+ \((.+)\)', low)
if intensity_match:
tier = intensity_match.group(1).lower()
tier_color = NAME_TIER_COLORS.get(tier, '#DDDDDD')
weapon_intensity_text = f"<basefont color={tier_color}>Magical Intensity: {raw_line.split(': ', 1)[1]}</basefont>"
debug_msg(f" ā WEAPON INTENSITY: {weapon_intensity_text}", COLORS['success'])
continue
# Check if this looks like a modifier FIRST (before slot-aware transformations)
# Exclude durability status lines from being treated as modifiers
is_modifier = (low in MODIFIER_PROPERTIES or
re.search(r'[+\-]\s*\d+', raw_line) or
any(keyword in low for keyword in ['damage', 'tactics', 'skill', 'accurate']))
debug_msg(f" Is durability status: {bool(is_durability_status)}", COLORS['cat'])
debug_msg(f" Is modifier: {is_modifier} (in MODIFIER_PROPERTIES: {low in MODIFIER_PROPERTIES})", COLORS['cat'])
if re.search(r'[+\-]\s*\d+', raw_line):
debug_msg(" Has +/- numbers: {}".format(re.search(r'[+\-]\s*\d+', raw_line).group()), COLORS['cat'])
modifier_keywords = [kw for kw in ['damage', 'tactics', 'skill', 'accurate'] if kw in low]
if modifier_keywords:
debug_msg(f" Has modifier keywords: {modifier_keywords}", COLORS['cat'])
# Apply slot-aware transformations (only for non-modifiers)
slot_ar_text = _compute_ar_bonus_text(equip_slot, low) if not is_modifier else None
slot_dura_text = _compute_durability_text(equip_slot, low) if not is_modifier else None
debug_msg(f" Slot AR text: {repr(slot_ar_text)}", COLORS['cat'])
debug_msg(f" Slot Dura text: {repr(slot_dura_text)}", COLORS['cat'])
if slot_ar_text:
debug_msg(f" Processing slot AR text: {repr(slot_ar_text)}", COLORS['cat'])
final_text = slot_ar_text
# AR bonuses are regular properties
color = _property_color_for_line(low)
debug_msg(f" AR color: {repr(color)}", COLORS['cat'])
safe_text = final_text.replace('<','<').replace('>','>') if '<basefont' not in final_text else final_text
formatted_line = f"<basefont color={color}>{safe_text}</basefont>"
regular_lines.append(formatted_line)
debug_msg(" ā REGULAR (AR): " + repr(formatted_line), COLORS['success'])
elif slot_dura_text:
final_text = slot_dura_text
# Durability is a regular property
color = _property_color_for_line(low)
safe_text = final_text.replace('<','<').replace('>','>') if '<basefont' not in final_text else final_text
formatted_line = f"<basefont color={color}>{safe_text}</basefont>"
regular_lines.append(formatted_line)
debug_msg(" ā REGULAR (Dura): " + repr(formatted_line), COLORS['success'])
elif is_durability_status:
# This is a durability status line - format with grey color
safe_text = raw_line.replace('<','<').replace('>','>')
formatted_line = f"<basefont color=#888888>{safe_text}</basefont>"
durability_lines.append(formatted_line)
debug_msg(" ā DURABILITY STATUS: " + repr(formatted_line), COLORS['info'])
elif is_modifier:
# This is a modifier property - use remapped text with HTML colors or format with colored text
if low in PROPERTY_REMAP:
# Check if this is an AR modifier placeholder that needs dynamic calculation
if PROPERTY_REMAP[low] == 'PLACEHOLDER_AR_MODIFIER':
final_text = _compute_ar_modifier_text(item_id, low)
# Special handling for "exceptional" - different for weapons vs armor
elif low == 'exceptional':
if is_weapon:
final_text = '<basefont color=#FF6B6B>+ 4 Damage</basefont> <basefont color=#FFB84D>( Exceptional )</basefont>'
else:
# For armor, exceptional gives durability bonus
final_text = '<basefont color=#888888>+ 20% Durability</basefont> <basefont color=#AAAAAA>( Exceptional )</basefont>'
# Accuracy set: use Archery for bows/crossbows instead of Tactics
elif low in ACCURACY_KEYS:
weapon_info = WEAPON_DATA_BY_ITEMID.get(item_id, {})
weapon_type = weapon_info.get('type', '') if weapon_info else ''
base_text = PROPERTY_REMAP[low]
if weapon_type in ('Bow', 'Crossbow'):
# Replace 'Tactics' with 'Archery' in the remapped string
final_text = base_text.replace('Tactics', 'Archery')
else:
final_text = base_text
else:
final_text = PROPERTY_REMAP[low]
# Don't escape HTML for remapped properties since they have color formatting
modifier_lines.append(final_text)
debug_msg(" ā MODIFIER (Remapped): " + repr(final_text), COLORS['warn'])
else:
# Format modifier with appropriate color category
formatted_text = format_modifier_text(raw_line)
modifier_lines.append(formatted_text)
debug_msg(" ā MODIFIER (Formatted): " + repr(formatted_text), COLORS['warn'])
else:
# Regular property - apply default color and escape HTML
final_text = PROPERTY_REMAP.get(low, raw_line)
safe_text = final_text.replace('<','<').replace('>','>') if '<basefont' not in final_text else final_text
color = _property_color_for_line(low)
formatted_line = f"<basefont color={color}>{safe_text}</basefont>"
regular_lines.append(formatted_line)
debug_msg(" ā REGULAR (Default): " + repr(formatted_line), COLORS['success'])
except Exception as prop_error:
debug_msg(f" ERROR processing property {prop_idx+1}: {prop_error}", COLORS['bad'])
continue
debug_msg("\nPROPERTY SEPARATION RESULTS:", COLORS['cat'])
debug_msg(f" Modifier lines ({len(modifier_lines)}):", COLORS['cat'])
for i, line in enumerate(modifier_lines):
debug_msg(f" [{i}] {repr(line)}", COLORS['cat'])
debug_msg(f" Regular lines ({len(regular_lines)}):", COLORS['cat'])
for i, line in enumerate(regular_lines):
debug_msg(f" [{i}] {repr(line)}", COLORS['cat'])
debug_msg(f" Durability lines ({len(durability_lines)}):", COLORS['cat'])
for i, line in enumerate(durability_lines):
debug_msg(f" [{i}] {repr(line)}", COLORS['cat'])
# Add weapon damage/speed section first for weapons (priority 5)
if is_weapon and (weapon_damage_text or weapon_speed_text):
weapon_combat_lines = []
if weapon_damage_text and weapon_speed_text:
# Combine damage and speed on one line
weapon_combat_lines.append(f"{weapon_damage_text} {weapon_speed_text}")
elif weapon_damage_text:
weapon_combat_lines.append(weapon_damage_text)
elif weapon_speed_text:
weapon_combat_lines.append(weapon_speed_text)
sections.append(TextSection(weapon_combat_lines, 'weapon_combat', 5))
debug_msg(f"ADDED SECTION: weapon_combat (priority 5, {len(weapon_combat_lines)} lines)", COLORS['warn'])
# Add modifier properties (priority 10)
if modifier_lines:
sections.append(TextSection(modifier_lines, 'modifiers', 10))
debug_msg(f"ADDED SECTION: modifiers (priority 10, {len(modifier_lines)} lines)", COLORS['warn'])
# Add artifact weapon description right below modifiers (priority 12)
try:
artifact_desc = resolve_artifact_weapon_description(item_id, item_display_name)
except Exception:
artifact_desc = None
if artifact_desc:
sep_needed_art = bool(modifier_lines)
sections.append(TextSection([f"<basefont color=#FF550F>Artifact: {artifact_desc}</basefont>"], 'artifact', 12, separator_before=sep_needed_art))
debug_msg(f"ADDED SECTION: artifact (priority 12, 1 line, separator: {sep_needed_art})", COLORS['info'])
# Add regular properties after modifiers (priority 15) with separator if modifiers exist
if regular_lines:
separator_needed = bool(modifier_lines) or bool(is_weapon and (weapon_damage_text or weapon_speed_text))
sections.append(TextSection(regular_lines, 'properties', 15, separator_before=separator_needed))
debug_msg(f"ADDED SECTION: properties (priority 15, {len(regular_lines)} lines, separator: {separator_needed})", COLORS['success'])
except Exception as e:
debug_msg(f"Error processing properties: {e}", COLORS['warn'])
# Add total AR section for armor items (priority 5 - above modifiers)
if item_id in ARMOR_DATA_BY_ITEMID:
armor_data = ARMOR_DATA_BY_ITEMID[item_id]
base_ar = armor_data.get('base_ar', 0)
ar_modifiers = armor_data.get('ar_modifiers', {})
# Calculate total AR - find the highest AR modifier that matches current item properties
total_ar = base_ar
current_ar_modifier = None
current_ar_bonus = 0
# Check which AR modifier is currently active on this item by looking at processed properties
for prop_line in (modifier_lines + regular_lines):
for modifier_name, modifier_total_ar in ar_modifiers.items():
if modifier_name.lower() in prop_line.lower():
if modifier_total_ar > current_ar_bonus: # Use highest AR modifier if multiple
current_ar_modifier = modifier_name
current_ar_bonus = modifier_total_ar
total_ar = modifier_total_ar # Use the total AR directly since it's stored as total
break
if total_ar > 0:
total_ar_line = f"<basefont color=#CCCCCC>{total_ar} AR </basefont>"
sections.append(TextSection([total_ar_line], 'total_ar', 5))
debug_msg(f"ADDED SECTION: total_ar (priority 5, 1 lines) - Base: {base_ar}, Modifier: {current_ar_modifier}({current_ar_bonus}), Total: {total_ar}", COLORS['info'])
# Add durability status last (priority 25) with separator if other sections exist - OUTSIDE try block
if durability_lines:
separator_needed = bool(modifier_lines or regular_lines)
sections.append(TextSection(durability_lines, 'durability', 25, separator_before=separator_needed))
debug_msg(f"ADDED SECTION: durability (priority 25, {len(durability_lines)} lines, separator: {separator_needed})", COLORS['info'])
# 2. Known item descriptions section (high priority) - hue-aware
try:
# Determine if hue should be ignored (weapons/armor can be any hue)
# Avoid calling is_weapon() here since a local variable named `is_weapon` may exist in this scope
treat_hue_as_agnostic = (get_weapon_data(item_id) is not None) or (get_armor_data(item_id) is not None)
try:
item_hue = int(getattr(target_item, 'Hue', 0) or 0)
except Exception:
item_hue = 0
known_lines = _resolve_known_item_lines(item_id, item_hue, treat_hue_as_agnostic=treat_hue_as_agnostic)
if known_lines:
debug_msg(
f"Adding {len(known_lines)} known item descriptions (hue-aware: {'yes' if not treat_hue_as_agnostic else 'no'})",
COLORS['cat']
)
# Show a separator before every known-item line except the first
have_rendered_any_known = False
need_separator_before_next = False
for i, desc_line in enumerate(known_lines):
# Allow inline separator control via markers in description list
raw = (desc_line or '').strip().lower()
if raw in ('---', '[sep]', '[separator]'):
debug_msg(f" Known desc [{i}] requested separator", COLORS['cat'])
need_separator_before_next = True
continue # Do not render a line for the marker itself
debug_msg(f" Known desc [{i}] (hue={item_hue}): {repr(desc_line)}", COLORS['cat'])
# Wrap line with default color, preserving existing basefont tags
formatted_line = _wrap_line_with_default_color(desc_line, '#BBBBBB')
debug_msg(f" Formatted: {repr(formatted_line)}", COLORS['cat'])
# Split long lines for word wrapping at 30 characters
wrapped_lines = _split_line_for_wrapping(formatted_line, 30)
# Add each line as its own section for better layout control
for j, wl in enumerate(wrapped_lines):
# Apply separator on the first wrapped visual line when:
# - We've already rendered at least one known line (separator between lines), or
# - A marker explicitly requested a separator
sep_flag = (j == 0) and ((have_rendered_any_known) or (need_separator_before_next))
sections.append(TextSection([wl], 'known', 8, separator_before=sep_flag))
if j == 0:
# After the first visual line of this description, we have rendered content
have_rendered_any_known = True
need_separator_before_next = False
# Ensure there is a separator before the next (non-known) section in the results
sections.append(TextSection([], 'known_end_separator', 9, separator_before=True))
debug_msg("ADDED known item lines as individual sections (priority 8) with dynamic separators and trailing separator", COLORS['info'])
except Exception as e:
debug_msg(f"Error adding known item descriptions: {e}", COLORS['warn'])
# 3. Equipment type section (medium priority)
equipment_lines = []
if equip_slot or friendly_type != 'Unknown':
equipment_lines.append(f"<basefont color=#BBBBBB>Type: {friendly_type}</basefont>")
# Weapon abilities - check weapon data dictionary directly
debug_msg(f"Checking weapon abilities: equip_slot='{equip_slot}', item_id={item_id} (0x{item_id:04X})", COLORS['cat'])
# Check if item is in weapon data
is_weapon = item_id in WEAPON_DATA_BY_ITEMID
weapon_data = WEAPON_DATA_BY_ITEMID.get(item_id)
debug_msg(f"Weapon lookup: is_weapon={is_weapon}, weapon_data={weapon_data}", COLORS['cat'])
if is_weapon:
debug_msg(f"Item is weapon, getting abilities...", COLORS['cat'])
primary_ability, secondary_ability = get_weapon_abilities(item_id)
debug_msg(f"Raw API result: primary='{primary_ability}', secondary='{secondary_ability}'", COLORS['cat'])
if primary_ability:
equipment_lines.append(f"<basefont color=#333333>Primary: {primary_ability}</basefont>") # previous color: #FFD700 (yellow)
if secondary_ability:
equipment_lines.append(f"<basefont color=#333333>Secondary: {secondary_ability}</basefont>") # previous color: #87CEEB (light blue)
else:
debug_msg(f"Item is not in weapon data dictionary", COLORS['cat'])
if equipment_lines:
sections.append(TextSection(equipment_lines, 'equipment_type', 30, separator_before=True))
debug_msg(f"ADDED SECTION: equipment_type (priority 30, {len(equipment_lines)} lines)", COLORS['cat'])
# 4. Weapon intensity section (second-to-last priority for weapons)
if is_weapon and weapon_intensity_text:
sections.append(TextSection([weapon_intensity_text], 'weapon_intensity', 35, separator_before=True))
debug_msg(f"ADDED SECTION: weapon_intensity (priority 35, 1 line)", COLORS['cat'])
# 5. Hue section (near bottom priority)
if hue_text:
sections.append(TextSection([hue_text], 'hue_info', 40, separator_before=True))
debug_msg(f"ADDED SECTION: hue_info (priority 40, 1 line)", COLORS['cat'])
# 6. Weapon skill section (last priority for weapons)
if is_weapon and weapon_skill_text:
sections.append(TextSection([weapon_skill_text], 'weapon_skill', 45, separator_before=True))
debug_msg(f"ADDED SECTION: weapon_skill (priority 45, 1 line)", COLORS['cat'])
# 7. Technical/dev info section (lowest priority) - shown when SHOW_TECHNICAL_INFO is True
if SHOW_TECHNICAL_INFO:
dev_lines = []
dev_lines.append(f"<basefont color=#999999>ItemID: {_fmt_hex4(item_id)}</basefont>")
if item_hue > 0:
dev_lines.append(f"<basefont color=#999999>Hue: {item_hue}</basefont>")
dev_lines.append(f"<basefont color=#999999>Serial: {item_serial}</basefont>")
if dev_lines:
sections.append(TextSection(dev_lines, 'technical_info', 50))
debug_msg(f"ADDED SECTION: technical_info (priority 50, {len(dev_lines)} lines)", COLORS['cat'])
# Sort by priority
debug_msg("\nSECTION ORDERING:", COLORS['cat'])
debug_msg(f" Before sorting ({len(sections)} sections):", COLORS['cat'])
for i, section in enumerate(sections):
debug_msg(f" [{i}] {section.category} (priority {section.priority}, {len(section.lines)} lines, separator: {section.separator_before})", COLORS['cat'])
sections.sort(key=lambda x: x.priority)
debug_msg(f" After sorting:", COLORS['cat'])
for i, section in enumerate(sections):
debug_msg(f" [{i}] {section.category} (priority {section.priority}, {len(section.lines)} lines, separator: {section.separator_before})", COLORS['cat'])
return sections
def show_walia_gump(target_item, usages: list, gump_id=None):
debug_msg(f"Showing results gump; usages={len(usages) if usages else 0}")
# Build modular text sections
text_sections = build_text_sections(target_item, usages)
debug_msg(f"Built {len(text_sections)} text sections")
item_display_name = get_item_name(target_item, amount_hint=getattr(target_item, 'Amount', 1))
item_graphic_id = int(getattr(target_item, 'ItemID', 0) or 0)
title_display_name = (item_display_name[:1].upper() + item_display_name[1:]) if item_display_name else "Unknown"
if DISPLAY.get('use_unicode_title', True):
try:
title_display_name = _stylize_unicode(title_display_name, 'fullwidth_nospace')
except Exception:
pass
title_text = f"{title_display_name}"
gump = Gumps.CreateGump(movable=True)
Gumps.AddPage(gump, 0)
# Base sizes with dynamic height based on text lines
max_rows_to_render = min(15, len(usages) or 1)
# Calculate height based on text sections
total_text_height = sum(section.height_estimate() for section in text_sections)
text_height_px = max(60, total_text_height)
# Calculate total content height
content_height_px = text_height_px
# Narrower layout and compact padding
gump_width = 320
# Dynamic height based on actual content - more precise calculation
base_padding = 20 # Top/bottom margins
title_space = 60 if DISPLAY.get('show_title', True) else 0 # Title section
item_graphic_space = 70 if DISPLAY.get('show_item_graphic', True) else 0 # Item graphic
crafting_space = (max_rows_to_render * 24) if DISPLAY.get('show_crafting', False) else 0
gump_height = base_padding + title_space + max(item_graphic_space, content_height_px) + crafting_space
debug_msg(f"Gump sizing: base={base_padding}, title={title_space}, content={content_height_px}, item_graphic={item_graphic_space}, crafting={crafting_space}, total={gump_height}", COLORS['cat'])
Gumps.AddBackground(gump, 0, 0, gump_width, gump_height, 30546)
Gumps.AddAlphaRegion(gump, 0, 0, gump_width, gump_height)
# Title section using unicode styling and HTML for better formatting
show_title_flag = DISPLAY.get('show_title', True)
content_top_y = 8
if show_title_flag:
# Use HTML for title to get better control over unicode styling
try:
property_list = Items.GetPropStringList(getattr(target_item, 'Serial', 0)) or []
name_hex_color = _derive_name_color(property_list)
except Exception:
name_hex_color = '#FFFFFF'
title_html = f"<center><basefont color={name_hex_color}><big><b>{title_text}</b></big></basefont></center>"
title_y = 10
# Check if title is long enough to wrap to second line
# Estimate: ~25-30 characters per line for <big><b> text in 300px width
title_length = len(title_text)
title_wraps = title_length > 20 # Conservative threshold for line wrapping
# Adjust title height and spacing if it wraps
if title_wraps:
title_height = 50 # Extra height for wrapped title
content_spacing = 52 # Extra spacing below title
debug_msg(f"Long title detected ({title_length} chars), using wrapped layout", COLORS['cat'])
else:
title_height = 30 # Normal title height
content_spacing = 32 # Normal spacing below title
debug_msg(f"Normal title length ({title_length} chars), using standard layout", COLORS['cat'])
Gumps.AddHtml(gump, 10, title_y, gump_width - 20, title_height, title_html, 0, 0)
content_top_y = title_y + content_spacing
# Item graphic on left
if DISPLAY.get('show_item_graphic', True):
item_top_y = content_top_y + 8 # Increased spacing from 4 to 8
# Try to apply hue if enabled and API supports it; otherwise fallback to no hue
try:
if DISPLAY.get('apply_item_hue', True):
Gumps.AddItem(gump, 20, item_top_y, item_graphic_id, getattr(target_item, 'Hue', 0))
else:
Gumps.AddItem(gump, 20, item_top_y, item_graphic_id)
except Exception:
Gumps.AddItem(gump, 20, item_top_y, item_graphic_id)
# Slightly larger layout reservation for item icon to avoid overlap
item_icon_height = 60
else:
item_top_y = content_top_y + 6
item_icon_height = 0
# Text content area to the right of the icon
additional_image_spacing_px = 8
text_x, text_y, text_width = 64 + additional_image_spacing_px, content_top_y + 2, (gump_width - 80)
# Render text sections separately to preserve HTML formatting
current_y = text_y + 2
text_offset_right_px = 4
debug_msg("\nFINAL HTML RENDERING:", COLORS['cat'])
debug_msg(f" Rendering {len(text_sections)} sections starting at Y={current_y}", COLORS['cat'])
for section_idx, section in enumerate(text_sections):
debug_msg("\n Section [{}]: {} (priority {})".format(section_idx, section.category, section.priority), COLORS['cat'])
debug_msg(f" Lines: {len(section.lines)}, Separator: {section.separator_before}, Y: {current_y}", COLORS['cat'])
if section.separator_before:
# Add separator line
separator_html = "<basefont color=#444444>āāāāāāāāā</basefont>"
debug_msg(f" Adding separator at Y={current_y}: {repr(separator_html)}", COLORS['cat'])
Gumps.AddHtml(
gump,
text_x + text_offset_right_px,
current_y,
text_width - text_offset_right_px,
18,
separator_html,
0,
0,
)
current_y += 18
# Render section content
section_html = section.to_html()
debug_msg(f" Generated HTML ({len(section_html) if section_html else 0} chars): {repr(section_html[:100])}{'...' if section_html and len(section_html) > 100 else ''}", COLORS['cat'])
if section_html:
section_height = len(section.lines) * 18
debug_msg(f" Rendering at Y={current_y}, height={section_height}", COLORS['cat'])
for line_idx, line in enumerate(section.lines):
debug_msg(f" Line [{line_idx}]: {repr(line)}", COLORS['cat'])
Gumps.AddHtml(
gump,
text_x + text_offset_right_px,
current_y,
text_width - text_offset_right_px,
section_height,
section_html,
0,
0,
)
current_y += section_height
# Track where content ends for crafting table positioning
content_bottom_y = current_y + 10
# Calculate item icon bottom for layout
item_bottom_y = item_top_y + item_icon_height if DISPLAY.get('show_item_graphic', True) else content_top_y
# Crafting section (table/messages) can be disabled entirely via master toggle
if DISPLAY.get('show_crafting', False):
# Only draw crafting table when usages exist AND at least one of Category/Makes is enabled
show_category_flag = DISPLAY.get('show_category', True)
show_makes_flag = DISPLAY.get('show_makes', True)
show_rarity_flag = DISPLAY.get('show_rarity', True)
if usages and len(usages) > 0 and (show_category_flag or show_makes_flag):
table_top_y = max(content_bottom_y, item_bottom_y)
# Column headers for narrower width
category_col_x = 10
makes_col_x = 92
rarity_col_x = max(188, gump_width - 90)
if show_category_flag:
Gumps.AddLabel(gump, category_col_x, table_top_y, COLORS['cat'], "Category")
if show_makes_flag:
Gumps.AddLabel(gump, makes_col_x, table_top_y, COLORS['cat'], "Makes")
if show_rarity_flag:
Gumps.AddLabel(gump, rarity_col_x, table_top_y, COLORS['cat'], "Rarity")
row_y = table_top_y + 16
# Sort by category then name
usages_sorted_list = sorted(usages, key=lambda r: ((r.get('category') or '').lower(), (r.get('name') or '').lower()))
for usage_record in usages_sorted_list[:max_rows_to_render]:
category_name = usage_record.get('category') or 'Unknown'
product_name = usage_record.get('name') or 'Unknown'
rarity_key = _rarity_for_recipe(usage_record)
rarity_hue = MATERIAL_RARITY[rarity_key]['hue']
if show_category_flag:
Gumps.AddLabel(gump, category_col_x, row_y, COLORS['label'], str(category_name))
# Product name (wrap limited width)
if show_makes_flag:
makes_col_width = max(120, gump_width - makes_col_x - 120)
Gumps.AddHtml(gump, makes_col_x, row_y-2, makes_col_width, 22, f"<basefont color=#FFFFFF>{product_name}</basefont>", 0, 0)
if show_rarity_flag:
Gumps.AddLabel(gump, rarity_col_x, row_y, rarity_hue, rarity_key.upper())
row_y += 24
else:
# Either no usages or crafting table is hidden by settings; place message below content without blocking
if DISPLAY.get('show_crafting_message', False):
row_y = max(content_bottom_y, item_bottom_y) + 8
if usages and len(usages) > 0 and not (show_category_flag or show_makes_flag):
message_text = "<basefont color=#9A9A9A>Crafting usages hidden (settings).</basefont>"
else:
message_text = "<basefont color=#D8D066>No known crafting usages found.</basefont>"
# Align with text panel to reduce excess padding
Gumps.AddHtml(gump, text_x, row_y, text_width, 22, message_text, 0, 0)
# Close button (optional based on global setting)
if SHOW_CLOSE_BUTTON:
Gumps.AddButton(gump, gump_width-30, 8, 4017, 4018, 1, 1, 0)
Gumps.AddTooltip(gump, "Close")
# Send gump with cycling ID (or reuse provided ID for DEV toggle)
current_gump_id = gump_id if gump_id is not None else get_next_results_gump_id()
Gumps.SendGump(current_gump_id, Player.Serial, RESULTS_X, RESULTS_Y, gump.gumpDefinition, gump.gumpStrings)
return current_gump_id
def send_launcher_gump():
"""Create and send a tiny floating gump with a single inspect button."""
debug_msg("Building launcher gump...")
gd = Gumps.CreateGump(movable=True)
Gumps.AddPage(gd, 0)
width, height = 75, 55
Gumps.AddBackground(gd, 0, 0, width, height, 30546)
Gumps.AddAlphaRegion(gd, 0, 0, width, height)
# Simple centered button (only one) over a clean background
try:
Gumps.AddItem(gd, 32, 26, 0x14F5) # centered-ish spyglass icon
except Exception:
pass
# Small hint label; background (not the button) is draggable
try:
Gumps.AddHtml(gd, 3, 0, 68, 16, "<center><basefont color=#3FA9FF>INFO</basefont></center>", 0, 0)
except Exception:
pass
Gumps.AddButton(gd, 5, 18, 9815, 9815, 1, 1, 0) # bookshelf
Gumps.AddTooltip(gd, "Get Item Info")
Gumps.SendGump(LAUNCHER_GUMP_ID, Player.Serial, LAUNCHER_X, LAUNCHER_Y, gd.gumpDefinition, gd.gumpStrings)
debug_msg("Launcher gump sent")
def process_launcher_input(index: dict) -> bool:
"""Handle clicks from the launcher. Returns False when launcher should close."""
Gumps.WaitForGump(LAUNCHER_GUMP_ID, 100)
gd = Gumps.GetGumpData(LAUNCHER_GUMP_ID)
if not gd:
return True # keep running; gump may not be visible yet
if gd.buttonid > 0:
# Only button id 1 exists: trigger inspection
if gd.buttonid == 1:
debug_msg("Launcher button clicked -> start targeting", 90)
global _IS_TARGETING
if _IS_TARGETING:
debug_msg("Ignored click: targeting already in progress", COLORS['warn'])
return True
_IS_TARGETING = True
try:
# Debounce to prevent the button click from leaking into the world click buffer
_pause(180)
walia_run_once(index)
finally:
_IS_TARGETING = False
# Re-send the launcher after action completes
send_launcher_gump()
return True
# Any other id could be a close; rebuild by default
send_launcher_gump()
return True
return True
def process_results_input():
"""Handle clicks from any of the cycling results gumps (close button only)."""
# Check all possible cycling gump IDs
for offset in range(RESULTS_GUMP_ID_MAX_OFFSET):
gump_id = RESULTS_GUMP_ID_BASE + offset
if not Gumps.HasGump(gump_id):
continue
Gumps.WaitForGump(gump_id, 5) # Short wait for each
gd = Gumps.GetGumpData(gump_id)
if not gd or gd.buttonid <= 0:
continue
# Process the button press and consume it
if gd.buttonid == 1:
# Close button pressed - close the gump
debug_msg("Close button pressed", COLORS['info'])
Gumps.CloseGump(gump_id)
return
def walia_run_once(index: dict):
# Prompt for a target using Razor Enhanced Target system
try:
Misc.SendMessage("Target an item to inspect usages...", COLORS['title'])
except Exception:
pass
debug_msg("Prompting for target")
try:
Target.Cancel()
_pause(100)
sel = Target.PromptTarget("Select an item to inspect")
except Exception:
sel = -1
if sel is None or sel < 0:
debug_msg("Target cancelled by user", COLORS['warn'])
try:
Misc.SendMessage("Target cancelled.", COLORS['warn'])
except Exception:
print("Target cancelled.")
return
try:
it = Items.FindBySerial(sel)
except Exception:
it = None
if not it:
debug_msg(f"Target serial not found: {sel}", COLORS['bad'])
try:
Misc.SendMessage("Could not find targeted item.", COLORS['bad'])
except Exception:
print("Could not find targeted item.")
return
debug_msg(f"Target acquired: serial={hex(sel)} id={_fmt_hex4(getattr(it,'ItemID',0))}")
usages = find_usages_for_item(it, index)
debug_msg(f"Usages found: {len(usages)}")
# cache last results for UI toggles
try:
global _LAST_TARGET_SERIAL, _LAST_TARGET_ITEMID, _LAST_TARGET_NAME, _LAST_USAGES
_LAST_TARGET_SERIAL = getattr(it, 'Serial', None)
_LAST_TARGET_ITEMID = getattr(it, 'ItemID', None)
_LAST_TARGET_NAME = get_item_name(it, amount_hint=getattr(it, 'Amount', 1))
_LAST_USAGES = usages[:]
except Exception:
pass
try:
show_walia_gump(it, usages)
except Exception as e:
try:
Misc.SendMessage(f"Error showing WALIA gump: {e}", COLORS['bad'])
except Exception:
print(f"Error showing WALIA gump: {e}")
def main():
# Load latest crafting crawl and build the material index once
# this needs rework for crafting recipes , currently focused on equipment
debug_msg("Starting WALIA")
_, data_dir = _script_root_paths()
src = _find_latest_crafting_json(data_dir)
if not src:
try:
Misc.SendMessage("No gump_crafting_*.json found in /data.", COLORS['bad'])
except Exception:
print("No gump_crafting_*.json found in /data.")
return
try:
debug_msg(f"Loading data from: {src}")
raw = _read_json(src)
items = _normalize_items(raw)
debug_msg(f"Items loaded: {len(items)}")
index = build_material_index(items)
debug_msg("Material index built")
# Send the persistent launcher and loop handling input
send_launcher_gump()
debug_msg("Entering UI loop")
while True:
_pause(50)
keep_running = process_launcher_input(index)
process_results_input()
if not keep_running:
break
except Exception as e:
try:
Misc.SendMessage(f"WALIA error: {e}", COLORS['bad'])
except Exception:
print(f"WALIA error: {e}")
if __name__ == "__main__":
main()
Version History
Version 1 - 9/13/2025, 3:21:42 AM - 3 days ago
UI_waila_item_info.py
Original Version Saved - 9/13/2025, 3:21:21 AM - 3 days ago