diff --git a/README.md b/README.md index 66bdc2d1..beb97421 100644 --- a/README.md +++ b/README.md @@ -42,7 +42,6 @@ The config folder contains: | [general] | Description | | ----------------------- | --------------------------------------| | profiles | A set of profiles seperated by comma. d4lf will look for these yaml files in config/profiles and in C:/Users/WINDOWS_USER/.d4lf/profiles. | -| whitelist_uniques | If True: Uniques will only be kept if they are found in the yaml profiles and match the requierments. If False: Uniques will be kept if they are found in the yaml and match the requierments or if they are not found at all in the profiles (default) | | run_vision_mode_on_startup | If the vision mode should automatically start when starting d4lf. Otherwise has to be started manually with the vision button or the hotkey. | | check_chest_tabs | How many chest tabs will be checked and fitlered for items in case chest is open when starting the filter. E.g. 2 will check first two chest tabs | | hidden_transparency | The overlay will go more transparent after not hovering it for a while. This can be any value between [0, 1] with 0 being completely invisible and 1 completely visible. Note the default "visible" transparancy is 0.89 | diff --git a/assets/aspects_unique.json b/assets/aspects_unique.json new file mode 100644 index 00000000..751cf478 --- /dev/null +++ b/assets/aspects_unique.json @@ -0,0 +1,79 @@ +{ + "banished_lords_talisman": "after you spend of your primary resource, your next core skill is guaranteed to overpower. your critical strikes that overpower deal increased damage.", + "fists_of_fate": "your attacks randomly deal to of their normal damage.", + "flickerstep": "each enemy you evade through reduces your active ultimate cooldown by seconds, up to seconds.", + "frostburn": "lucky hit up to an chance to freeze enemies for seconds.", + "godslayer_crown": "when you stun, freeze, or immobilize an elite enemy, or damage a boss, it pulls in nearby enemies. you deal increased damage to them for seconds. this effect can only occur once every seconds.", + "mothers_embrace": "if a core skill hits or more enemies, of the resource cost is refunded.", + "penitent_greaves": "you leave behind a trail of frost that chills enemies. you deal more damage to chilled enemies.", + "razorplate": "gain thorns.", + "soulbrand": "your healing potion no longer heals instantly, instead it grants a barrier for of the healing for seconds. while you have a barrier, you gain damage reduction.", + "tassets_of_the_dawning_sky": "when you take damage from a nonphysical damage type, you gain maximum resistance to that damage type for seconds. this effect can only apply to one damage type at a time.", + "temerity": "effects that heal you beyond life grant you a barrier up to of your maximum life that lasts for seconds.", + "the_butchers_cleaver": "lucky hit when you critically strike an enemy you have up to a chance to fear and slow them by for seconds.", + "tibaults_will": "you deal increased damage while unstoppable and for seconds after. when you become unstoppable, gain of your primary resource.", + "xfals_corroded_signet": "lucky hit your damage over time effects have up to a chance to erupt, dealing damage of the same type to nearby enemies.", + "ahavarion_spear_of_lycander": "gain a random shrine effect for seconds after killing an elite enemy. can only occur once every seconds.", + "andariels_visage": "up to an chance to trigger a poison nova that applies poisoning damage over seconds to enemies in the area.", + "doombringer": "lucky hit up to an chance to deal shadow damage to surrounding enemies and reduce their damage done by for seconds.", + "harlequin_crest": "gain damage reduction. in addition, gain ranks to all skills.", + "melted_heart_of_selig": "gain maximum resource. in addition, when you take damage, drain resource for every of life you would have lost instead.", + "ring_of_starless_skies": "each consecutive core skill cast reduces the resource cost of your next core skill by, up to a maximum of.", + "the_grandfather": "increases your critical strike damage by.", + "100000_steps": "after gaining the final damage bonus from the walking arsenal key passive, you automatically cast ground stomp and gain fury. this cannot happen more than once every seconds.", + "ancients_oath": "steel grasp launches additional chains. enemies that have been pulled by steel grasp take xx bonus damage from you for seconds.", + "azurewrath": "lucky hit your core skills have up to a chance to freeze enemies for seconds and deal cold damage to them.", + "battle_trance": "increase frenzys maximum stacks by. while you have maximum frenzy, your other skills gain increased attack speed.", + "fields_of_crimson": "while using this weapon, damaging at least one enemy with rupture creates a blood pool that inflicts bleeding damage over seconds. enemies standing in the pool take increased bleeding damage.", + "gohrs_devastating_grips": "whirlwind explodes after it ends, dealing of the total base damage dealt to surrounding enemies as fire damage.", + "hellhammer": "upheaval ignites the ground, burning enemies for an additional damage over seconds.", + "overkill": "death blow creates a shockwave, dealing of its base damage to enemies. enemies who die to this effect also reset death blows cooldown.", + "rage_of_harrogath": "lucky hit up to a nx chance to reduce the cooldowns of your nonultimate skills by seconds when you inflict bleeding on elites.", + "ramaladnis_magnum_opus": "skills using this weapon deal increased damage per point of fury you have, but you lose fury every second.", + "ring_of_red_furor": "after spending fury within seconds, your next cast of hammer of the ancients, upheaval, or death blow is a guaranteed critical strike and deals (multiplicative damage) bonus critical strike damage", + "tuskhelm_of_joritz_the_mighty": "when you gain berserking while already berserk, you have a chance to become more enraged granting (multiplicative damage) increased damage, fury per second, and a cooldown reduction", + "airidahs_inexorable_will": "when casting an ultimate skill and again seconds after, pull in distant enemies and deal physical damage to them. this damage is increased by per point of willpower you have.", + "dolmen_stone": "casting boulder while hurricane is active will cause your boulders to rotate around you.", + "fleshrender": "debilitating roar and blood howl deal damage to nearby poisoned enemies. deals increased damage based on how much willpower you have", + "greatstaff_of_the_crone": "claw is now a storm skill and also casts storm strike at normal damage.", + "hunters_zenith": "gain a bonus when you kill with a shapeshifting skill. werewolf your next nonultimate werebear skill costs no resource and has no cooldown. werebear your next werewolf skill will heal you for when damage is first dealt.", + "insatiable_fury": "werebear form is now your true form, and you gain ranks to all werebear skills.", + "mad_wolfs_glee": "werewolf form is now your true form, and you gain ranks to all werewolf skills.", + "storms_companion": "your wolf companions are infused with the power of the storm, dealing lightning damage and gaining the storm howl ability.", + "tempest_roar": "lucky hit storm skills have up to a chance to grant spirit.", + "vasilys_prayer": "your earth skills are now also werebear skills and fortify you for.", + "waxing_gibbous": "gain stealth for seconds when killing enemies with shred. breaking stealth with an attack grants ambush, which guarantees critical strikes for seconds.", + "black_river": "corpse explosion consumes up to additional corpses around the initial corpse, dealing increased damage and with a larger radius per additional corpse.", + "blood_artisans_cuirass": "when you pick up blood orbs, a free bone spirit is spawned, dealing bonus damage based on your current life percent.", + "blood_moon_breeches": "your minions have a chance to curse enemies. enemies affected by at least of your curses take (multiplicative damage) increased overpower damage from you.", + "bloodless_scream": "your darkness skills chill enemies for up to. lucky hit your darkness skills have up to a chance to generate additional essence against frozen targets. darkness skills deal xx bonus damage to frozen enemies", + "deathless_visage": "bone spear leaves behind echoes as it travels that explode, dealing damage. echoes deal increased damage based on your critical strike bonus damage stat", + "deathspeakers_pendant": "blood surge casts a mini nova on your minions, dealing damage. damage is increased by per target drained by the initial cast, up to.", + "greaves_of_the_empty_tomb": "create desecrated ground beneath your sever spectrers as they travel, damaging enemies for shadow damage over seconds.", + "howl_from_below": "instead of detonating immediately, corpse explosion summons a volatile skeleton that charges at a random enemy and explodes. corpse explosions damage is increased by.", + "lidless_wall": "lucky hit while you have an active bone storm, hitting an enemy outside of a bone storm has up to a chance to spawn an additional bone storm at their location. each of your active sacrifice bonuses increases the chance by and the total number of additional bone storms you can have by", + "ring_of_mendeln": "lucky hit up to a chance to empower all of your minions, causing the next attack from each to explode for physical damage.", + "ring_of_sacrilegious_soul": "automatically activate the following equipped skills on corpses around you raise skeleton every seconds, corpse explosion every seconds, corpse tendrils every seconds.", + "ashearas_khanjar": "hits with this weapon increase your attack speed by for seconds, up to.", + "condemnation": "your core skills deal increased damage when spending combo points. your basic skills using this weapon have a chance to generate combo points.", + "cowl_of_the_nameless": "you gain increased lucky hit chance against crowd controlled enemies.", + "eaglehorn": "penetrating shot has a chance to fire an arrow that bounces off walls and scenery. damaging enemies with penetrating shot will cause your next cast to make enemies hit vulnerable for seconds", + "eyes_in_the_dark": "unless it hits a boss or players, death trap will continue to rearm itself until it kills an enemy. death trap deals bonus damage and this unique increases all damage to enemies affected by trap skill by. however, death traps cooldown is increased by.", + "grasp_of_shadow": "lucky hit damaging a vulnerable enemy with a marksman or cutthroat skill has up to an chance to summon a shadow clone that mimics your attack.", + "scoundrels_leathers": "while you have unlimited energy from inner sight, your core skills have a chance to spawn caltrops, poison trap, or death trap.", + "skyhunter": "the first direct damage you deal to an enemy is a guaranteed critical strike. if you had maximum stacks of the precision key passive when you cast a core skill, then that skill gains bonus critical strike damage and you gain energy.", + "windforce": "lucky hit hits with this weapon have up to a chance to deal double damage and knock back the target.", + "word_of_hakan": "your rain of arrows is always imbued with all imbuements at once.", + "writhing_band_of_trickery": "casting a subterfuge skill leaves behind a decoy trap that continuously taunts and lures enemies. the decoy trap explodes after seconds dealing shadow damage. can occur every seconds.", + "blue_rose": "lucky hit damaging an enemy has up to a chance of forming an exploding ice spike, dealing cold damage. triple this chance if the enemy is frozen.", + "esadoras_overflowing_cameo": "upon collecting crackling energy, theres a chance to release a lightning nova, dealing lightning damage. lightning nova deals additional damage based on your intelligence stat.", + "esus_heirloom": "your critical strike chance is increased by of your movement speed bonus.", + "flamescar": "while channeling incinerate, you periodically shoot embers that are attracted to enemies, each dealing fire damage.", + "gloves_of_the_illuminator": "fireball now bounces as it travels, exploding each time it hits the ground, but its explosion deals less damage.", + "iceheart_brais": "enemies that die while frozen have an chance to unleash a frost nova.", + "rainment_of_the_infinite": "after using teleport, close enemies are pulled to you and stunned for seconds, but teleports cooldown is increased by.", + "staff_of_endless_rage": "every rd cast of fireball launches additional projectiles and deals bonus damage.", + "staff_of_lam_esen": "your cast of charged bolts have a chance to be attracted to enemies and last longer", + "tal_rashas_iridescent_loop": "for each type of elemental damage you deal, gain increased damage for seconds. dealing elemental damage refreshes all bonuses.", + "the_oculus": "gain the effect of the teleport enchantment for free. when you evade using teleport enchantment, you are taken to a random location." +} \ No newline at end of file diff --git a/config/params.ini b/config/params.ini index e6bc2a48..ea341d39 100644 --- a/config/params.ini +++ b/config/params.ini @@ -1,13 +1,7 @@ [general] ; Which filter profiles should be run. All .yaml files with "Aspects" and "Affixes" sections will be used from ; config/profiles/*.yaml and C:/Users/USERNAME/.d4lf/profiles/*.yaml -profiles=general,barb,druid,necro,rogue,sorc,uniq_general,uniq_barb,uniq_druid,uniq_rogue,uniq_necro,uniq_sorc -; Unique filters can have either whitelist or blacklist behaviour -; If whitelist is True: Uniques will only be kept if a match was found to some yaml config and it passes the threshold. -; NOTE: If any affix on the yaml config is wrong or you forgot to add that unique to the .yaml, it will be discarded -; If whitelist is False: All uniques will be kept unless a match was found to a yaml config. If match was found but -; the requierments are not ok, it will be discarded -whitelist_uniques=False +profiles=general,barb,druid,necro,rogue,sorc,uniques ; weather to run vision mode on startup or not run_vision_mode_on_startup=True ; How many tabs should be checked for items in chest. Note: All 5 Tabs must be unlocked! diff --git a/config/profiles/uniq_barb.yaml b/config/profiles/uniq_barb.yaml deleted file mode 100644 index 4ea0d8cd..00000000 --- a/config/profiles/uniq_barb.yaml +++ /dev/null @@ -1,110 +0,0 @@ -# Source: https://d4builds.gg/database/uniques/ - -Uniques: - - 100000 Steps: - itemType: boots - minPower: 860 - affixPool: - - [dexterity] - - [damage_with_skills_that_swap_to_new_weapons] - - [movement_speed] - - [ranks_of_ground_stomp] - aspectValue: 32 - - - Ancients Oath: - itemType: boots - minPower: 860 - affixPool: - - [damage_to_close_enemies] - - [vulnerable_damage] - - [damage_while_berserking] - - [ranks_of_steel_grasp] - aspectValue: 30 - - - Azurewrath: - itemType: sword - minPower: 900 - affixPool: - - [attack_speed] - - [core_skill_damage] - - [nonphysical_damage] - - [damage_to_crowd_controlled_enemies] - aspectValue: 30 - - - Battle Trance: - itemType: amulet - minPower: 860 - affixPool: - - [cooldown_reduction] - - [damage_reduction_from_close_enemies] - - [maximum_fury] - - [ranks_of_frenzy] - aspectValue: 35 - - - Fields of Crimson: - itemType: two-handed sword - minPower: 900 - affixPool: - - [damage_with_twohanded_slashing_weapons] - - [damage_over_time] - - [critical_strike_damage] - - [rupture_cooldown_reduction] - - - Gohrs Devastating Grips: - itemType: gloves - minPower: 850 - affixPool: - - [critical_strike_chance_against_close_enemies] - - [lucky_hit_chance] - - [damage] - - [ranks_of_whirlwind] - aspectValue: 32 - - - Hellhammer: - itemType: two-handed mace - minPower: 900 - affixPool: - - [damage_with_twohanded_bludgeoning_weapons] - - [damage_to_burning_enemies] - - [damage_reduction_from_enemies_that_are_burning] - - [ranks_of_upheaval] - - - Overkill: - itemType: two-handed mace - minPower: 900 - affixPool: - - [physical_damage] - - [overpower_damage] - - [damage_to_injured_enemies] - - [ranks_of_death_blow] - aspectValue: 24 - - - Rage of Harrogath: - itemType: chest armor - minPower: 850 - affixPool: - - [physical_damage] - - [critical_strike_chance_with_physical_damage_against_elites] - - [damage_reduction_from_enemies_that_are_bleeding] - - [thorns] - aspectValue: 20 - - - Ramaladnis Magnum Opus: - itemType: sword - minPower: 900 - affixPool: - - [damage_with_dualwielded_weapons] - - [damage_to_close_enemies] - - [maximum_fury] - - [lucky_hit_up_to_a_chance_to_restore_primary_resource] - aspectValue: 0.2 - - - Tuskhelm of Joritz the Mighty: - itemType: helm - minPower: 850 - affixPool: - - [maximum_fury] - - [attack_speed] - - [damage_while_berserking] - - [ranks_of_the_aggressive_resistance_passive] - aspectValue: 40 diff --git a/config/profiles/uniq_druid.yaml b/config/profiles/uniq_druid.yaml deleted file mode 100644 index 8087533d..00000000 --- a/config/profiles/uniq_druid.yaml +++ /dev/null @@ -1,95 +0,0 @@ -# Source: https://d4builds.gg/database/uniques/ - -Uniques: - - Dolmen Stone: - itemType: amulet - minPower: 870 - affixPool: - - [maximum_life] - - [resource_generation] - - [nature_magic_skill_cooldown_reduction] - - [ranks_of_all_wrath_skills] - - - Fleshrender: - itemType: mace - minPower: 900 - affixPool: - - [damage_while_shapeshifted] - - [damage_to_poisoned_enemies] - - [core_skill_damage] - - [ranks_of_all_defensive_skills] - - - Greatstaff of the Crone: - itemType: staff - minPower: 900 - affixPool: - - [damage_to_close_enemies] - - [damage_to_crowd_controlled_enemies] - - [nonphysical_damage] - - [ranks_of_claw] - aspectValue: 120 - - - Hunters Zenith: - itemType: ring - minPower: 860 - affixPool: - - [damage_while_shapeshifted] - - [critical_strike_chance] - - [ranks_of_the_heightened_senses_passive] - - [ranks_of_the_quickshift_passive] - - - Insatiable Fury: - itemType: chest armor - minPower: 850 - affixPool: - - [physical_damage] - - [overpower_damage] - - [damage_reduction_while_fortified] - - [total_armor_while_in_werebear_form] - - - Mad Wolfs Glee: - itemType: chest armor - minPower: 860 - affixPool: - - [maximum_life] - - [poison_damage] - - [damage_reduction_from_enemies_that_are_poisoned] - - [movement_speed] - - - Storms Companion: - itemType: pants - minPower: 850 - affixPool: - - [maximum_life] - - [damage_reduction] - - [companion_movement_speed] - - [ranks_of_wolves] - - - Tempest Roar: - itemType: helm - minPower: 850 - affixPool: - - [damage_while_shapeshifted] - - [critical_strike_damage] - - [maximum_spirit] - - [poison_resistance] - aspectValue: 15 - - - Vasilys Prayer: - itemType: helm - minPower: 870 - affixPool: - - [damage_while_shapeshifted] - - [overpower_damage] - - [maximum_life] - - [lightning_resistance] - - - Waxing Gibbous: - itemType: axe - minPower: 900 - affixPool: - - [critical_strike_damage] - - [damage_to_close_enemies] - - [damage_to_injured_enemies] - - [life_on_kill] - aspectValue: 1.0 diff --git a/config/profiles/uniq_general.yaml b/config/profiles/uniq_general.yaml deleted file mode 100644 index 686ee76e..00000000 --- a/config/profiles/uniq_general.yaml +++ /dev/null @@ -1,201 +0,0 @@ -# Source: https://d4builds.gg/database/uniques/ - -Uniques: - # Ubers - # ========================= - - Ahavarion Spear of Lycander: - itemType: staff - minPower: - affixPool: - - [critical_strike_chance] - - [attack_speed] - - [damage] - - [lucky_hit_up_to_a_chance_to_stun] - - - Andariels Visage: - itemType: helm - minPower: - affixPool: - - [all_stats] - - [attack_speed] - - [life_steal] - - [poison_resistance] - - - Doombringer: - itemType: sword - minPower: - affixPool: - - [core_skill_damage] - - [damage] - - [lucky_hit_up_to_a_chance_to_heal_life] - - [maximum_life] - - - Harlequin Crest: - itemType: helm - minPower: - affixPool: - - [maximum_life] - - [cooldown_reduction] - - [resource_generation] - - [all_stats] - - - - Melted Heart of Selig: - itemType: amulet - minPower: - affixPool: - - [all_stats] - - [core_skill_damage] - - [damage_while_healthy] - - [resource_regeneration] - - - Ring of Starless Skies: - itemType: ring - minPower: - affixPool: - - [lucky_hit_chance] - - [critical_strike_chance] - - [critical_strike_damage] - - [core_skill_damage] - - - The Grandfather: - itemType: two-handed sword - minPower: - affixPool: - - [damage] - - [maximum_life] - - [all_stats] - - [ignores_durability_loss] - - - # Others - # ========================== - # NOTE: We can not filter for Razorplate as it has no affixes! - - Frostburn: - itemType: gloves - minPower: 900 - affixPool: - - [attack_speed] - - [critical_strike_chance] - - [freeze_duration] - - [lucky_hit_up_to_a_chance_to_restore_primary_resource] - aspectValue: 20 - - - Banished Lords Talisman: - itemType: amulet - minPower: 900 - affixPool: - - [critical_strike_chance] - - [overpower_damage] - - [resource_generation] - - [ranks_of_all_core_skills] - aspectValue: 100 - - - Godslayer Crown: - itemType: helm - minPower: 900 - affixPool: - - [cooldown_reduction] - - [crowd_control_duration] - - [maximum_life] - - [damage] - aspectValue: 60 - - - Fists of Fate: - itemType: gloves - minPower: 850 - affixPool: - - [lucky_hit_up_to_a_chance_to_gain_damage_for_seconds] - - [lucky_hit_up_to_a_chance_to_restore_primary_resource] - - [lucky_hit_up_to_a_chance_to_immobilize] - - [lucky_hit_up_to_a_chance_to_daze] - aspectValue: 279 - - - Mothers Embrace: - itemType: ring - minPower: 850 - affixPool: - - [all_stats] - - [lucky_hit_chance] - - [critical_strike_damage] - - [maximum_life] - aspectValue: 40 - - - Penitent Greaves: - itemType: boots - minPower: 850 - affixPool: - - [movement_speed] - - [crowd_control_duration] - - [slow_duration_reduction] - - [cold_resistance] - aspectValue: 9 - - - Soulbrand: - itemType: chest armor - minPower: 900 - affixPool: - - [lucky_hit_chance_while_you_have_a_barrier] - - [barrier_generation] - - [maximum_life] - - [damage_reduction_from_close_enemies] - aspectValue: 20 - - - Tassets of the Dawning Sky: - itemType: pants - minPower: 900 - affixPool: - - [control_impaired_duration_reduction] - - [maximum_life] - - [all_stats] - - [resistance_to_all_elements] - aspectValue: 10 - - - Temerity: - itemType: pants - minPower: 900 - affixPool: - - [maximum_life] - - [potion_drop_rate] - - [lucky_hit_up_to_a_chance_to_heal_life] - - [healing_received] - aspectValue: 70 - - - Tibaults Will: - itemType: pants - minPower: 900 - affixPool: - - [damage_reduction_from_close_enemies] - - [potion_capacity] - - [damage] - - [maximum_resource] - aspectValue: 30 - - - The Butchers Cleaver: - itemType: axe - minPower: 900 - affixPool: - - [physical_damage] - - [damage_to_crowd_controlled_enemies] - - [critical_strike_damage] - - [critical_strike_chance_against_injured_enemies] - aspectValue: 70 - - - Flickerstep: - itemType: boots - minPower: 820 - affixPool: - - [all_stats] - - [movement_speed] - - [ultimate_skill_damage] - - [damage_reduction_from_close_enemies] - aspectValue: 4 - - - XFals Corroded Signet: - itemType: ring - minPower: 870 - affixPool: - - [physical_damage] - - [damage_to_crowd_controlled_enemies] - - [critical_strike_damage] - - [critical_strike_chance_against_injured_enemies] diff --git a/config/profiles/uniq_necro.yaml b/config/profiles/uniq_necro.yaml deleted file mode 100644 index 1fce955c..00000000 --- a/config/profiles/uniq_necro.yaml +++ /dev/null @@ -1,98 +0,0 @@ -# Source: https://d4builds.gg/database/uniques/ - -Uniques: - - Black River: - itemType: scythe - minPower: 900 - affixPool: - - [intelligence] - - [ranks_of_all_corpse_skills] - - [ranks_of_the_hewed_flesh_passive] - - [ranks_of_the_fueled_by_death_passive] - aspectValue: - - - Blood Artisans Cuirass: - itemType: chest armor - minPower: 850 - affixPool: - - [damage_for_seconds_after_picking_up_a_blood_orb] - - [blood_orb_healing] - - [maximum_life] - - [ranks_of_bone_spirit] - aspectValue: [10, "smaller"] - - - Blood Moon Breeches: - itemType: pants - minPower: 850 - affixPool: - - [maximum_life] - - [ranks_of_all_curse_skills] - - [ranks_of_the_amplify_damage_passive] - - [damage_reduction_from_enemies_affected_by_curse_skills] - aspectValue: 3 - - - Bloodless Scream: - itemType: two-handed scythe - minPower: 900 - affixPool: - - [darkness_skill_damage] - - [damage_to_chilled_enemies] - - [intelligence] - - [cold_resistance] - aspectValue: 20 - - - Deathless Visage: - itemType: helm - minPower: 850 - affixPool: - - [damage_reduction] - - [physical_damage] - - [critical_strike_damage_with_bone_skills] - - [maximum_essence] - - - Deathspeakers Pendant: - itemType: amulet - minPower: 870 - affixPool: - - [blood_skill_damage] - - [summoning_skill_damage] - - [essence_cost_reduction] - - [ranks_of_the_coalesced_blood_passive] - - - Greaves of the Empty Tomb: - itemType: boots - minPower: 850 - affixPool: - - [movement_speed] - - [essence_cost_reduction] - - [lucky_hit_chance_with_shadow_damage] - - [damage_reduction_from_enemies_that_are_affected_by_shadow_damage_over_time] - - - Howl from Below: - itemType: gloves - minPower: 860 - affixPool: - - [lucky_hit_chance] - - [corpse_skill_attack_speed] - - [lucky_hit_up_to_a_chance_to_stun] - - [lucky_hit_up_to_a_chance_to_fear] - aspectValue: 30 - - - Lidless Wall: - itemType: shield - minPower: 860 - affixPool: - - [attack_speed] - - [maximum_life] - - [maximum_essence] - - [lucky_hit_up_to_a_chance_to_restore_primary_resource] - aspectValue: 10 - - - Ring of Mendeln: - itemType: ring - minPower: 860 - affixPool: - - [lucky_hit_chance] - - [minion_attack_speed] - - [maximum_minion_life] - - [thorns] diff --git a/config/profiles/uniq_rogue.yaml b/config/profiles/uniq_rogue.yaml deleted file mode 100644 index 264aaf18..00000000 --- a/config/profiles/uniq_rogue.yaml +++ /dev/null @@ -1,101 +0,0 @@ -# Source: https://d4builds.gg/database/uniques/ - -Uniques: - - Cowl of the Nameless: - itemType: helm - minPower: 900 - affixPool: - - [crowd_control_duration] - - [damage_reduction_from_close_enemies] - - [cooldown_reduction] - - [ranks_of_all_imbuement_skills] - aspectValue: 20 - - - Scoundrels Leathers: - itemType: chest armor - minPower: 900 - affixPool: - - [trap_skill_damage] - - [damage_to_enemies_affected_by_trap_skills] - - [damage_reduction_from_enemies_affected_by_trap_skills] - - [dodge_chance] - aspectValue: 70 - - - Skyhunter: - itemType: bow - minPower: 900 - affixPool: - - [dexterity] - - [critical_strike_damage] - - [marksman_skill_damage] - - [ranks_of_the_exploit_passive] - aspectValue: 15 - - - Windforce: - itemType: bow - minPower: 900 - affixPool: - - [vulnerable_damage] - - [core_skill_damage] - - [ranks_of_the_impetus_passive] - - [ranks_of_the_concussive_passive] - aspectValue: 25 - - - Eaglehorn: - itemType: bow - minPower: 900 - affixPool: - - [critical_strike_chance] - - [vulnerable_damage] - - [physical_damage] - - [damage_to_elites] - aspectValue: 70 - - - Condemnation: - itemType: dagger - minPower: 900 - affixPool: - - [basic_skill_attack_speed] - - [critical_strike_damage] - - [core_skill_damage] - - [damage_with_dualwielded_weapons] - aspectValue: 30 - - - Grasp Of Shadow: - itemType: gloves - minPower: 900 - affixPool: - - [dexterity] - - [attack_speed] - - [shadow_clone_damage] - - [ranks_of_all_core_skills] - aspectValue: 29 - - - Eyes in the Dark: - itemType: pants - minPower: 900 - affixPool: - - [shadow_damage] - - [damage_to_enemies_affected_by_trap_skills] - - [maximum_life] - - [damage_reduction] - aspectValue: 40 - - - Word of Hakan: - itemType: amulet - minPower: 860 - affixPool: - - [movement_speed] - - [ultimate_skill_damage] - - [rain_of_arrows_skill_cooldown_reduction] - - [ranks_of_all_imbuement_skills] - - - Ashearas Khanjar: - itemType: dagger - minPower: 900 - affixPool: - - [basic_skill_damage] - - [damage_to_crowd_controlled_enemies] - - [movement_speed_for_seconds_after_killing_an_elite] - - [lucky_hit_chance] - aspectValue: 20 \ No newline at end of file diff --git a/config/profiles/uniq_sorc.yaml b/config/profiles/uniq_sorc.yaml deleted file mode 100644 index f0c0f4b2..00000000 --- a/config/profiles/uniq_sorc.yaml +++ /dev/null @@ -1,88 +0,0 @@ -# Source: https://d4builds.gg/database/uniques/ - -Uniques: - - Blue Rose: - itemType: ring - minPower: 860 - affixPool: - - [critical_strike_damage] - - [ice_spike_damage] - - [lucky_hit_chance] - - [mana_cost_reduction] - aspectValue: 0.25 - - - Esadoras Overflowing Cameo: - itemType: amulet - minPower: 860 - affixPool: - - [cooldown_reduction] - - [crackling_energy_damage] - - [ranks_of_the_shocking_impact_passive] - - [movement_speed] - - - Esus Heirloom: - itemType: boots - minPower: 850 - affixPool: - - [movement_speed] - - [damage_for_seconds_after_killing_an_elite] - - [shrine_buff_duration] - - [critical_strike_damage] - aspectValue: 20 - - - Flamescar: - itemType: wand - minPower: 900 - affixPool: - - [mana_cost_reduction] - - [damage_to_burning_enemies] - - [lucky_hit_chance_with_fire_damage] - - [ranks_of_incinerate] - - - Gloves of the Illuminator: - itemType: gloves - minPower: 860 - affixPool: - - [critical_strike_chance] - - [fireball_attack_speed] - - [lucky_hit_up_to_a_chance_to_restore_primary_resource] - - [ranks_of_fireball] - aspectValue: [35, "smaller"] - - - Iceheart Brais: - itemType: pants - minPower: 850 - affixPool: - - [maximum_life] - - [damage_to_frozen_enemies] - - [damage_reduction] - - [freeze_duration] - aspectValue: 11 - - - Raiment of the Infinite: - itemType: chest armor - minPower: 850 - affixPool: - - [intelligence] - - [damage_to_close_enemies] - - [damage_to_stunned_enemies] - - [ranks_of_the_glass_cannon_passive] - aspectValue: 2 - - - Staff of Endless Rage: - itemType: staff - minPower: 850 - affixPool: - - [core_skill_damage] - - [damage_to_close_enemies] - - [fire_damage_ranks_of_the_inner_flames_passive] - aspectValue: 20 - - - The Oculus: - itemType: staff - minPower: 900 - affixPool: - - [max_evade_charges] - - [attacks_reduce_evades_cooldown_by_seconds] - - [damage] - - [ranks_of_teleport] diff --git a/config/profiles/uniques.yaml b/config/profiles/uniques.yaml new file mode 100644 index 00000000..5b6a84f6 --- /dev/null +++ b/config/profiles/uniques.yaml @@ -0,0 +1,309 @@ +Uniques: + - aspect: [banished_lords_talisman] + minPower: + affixPool: + + - aspect: [fists_of_fate] + minPower: + affixPool: + + - aspect: [flickerstep] + minPower: + affixPool: + + - aspect: [frostburn] + minPower: + affixPool: + + - aspect: [godslayer_crown] + minPower: + affixPool: + + - aspect: [mothers_embrace] + minPower: + affixPool: + + - aspect: [penitent_greaves] + minPower: + affixPool: + + - aspect: [razorplate] + minPower: + affixPool: + + - aspect: [soulbrand] + minPower: + affixPool: + + - aspect: [tassets_of_the_dawning_sky] + minPower: + affixPool: + + - aspect: [temerity] + minPower: + affixPool: + + - aspect: [the_butchers_cleaver] + minPower: + affixPool: + + - aspect: [tibaults_will] + minPower: + affixPool: + + - aspect: [xfals_corroded_signet] + minPower: + affixPool: + + - aspect: [ahavarion_spear_of_lycander] + minPower: + affixPool: + + - aspect: [andariels_visage] + minPower: + affixPool: + + - aspect: [doombringer] + minPower: + affixPool: + + - aspect: [harlequin_crest] + minPower: + affixPool: + + - aspect: [melted_heart_of_selig] + minPower: + affixPool: + + - aspect: [ring_of_starless_skies] + minPower: + affixPool: + + - aspect: [the_grandfather] + minPower: + affixPool: + + - aspect: [100000_steps] + minPower: + affixPool: + + - aspect: [ancients_oath] + minPower: + affixPool: + + - aspect: [azurewrath] + minPower: + affixPool: + + - aspect: [battle_trance] + minPower: + affixPool: + + - aspect: [fields_of_crimson] + minPower: + affixPool: + + - aspect: [gohrs_devastating_grips] + minPower: + affixPool: + + - aspect: [hellhammer] + minPower: + affixPool: + + - aspect: [overkill] + minPower: + affixPool: + + - aspect: [rage_of_harrogath] + minPower: + affixPool: + + - aspect: [ramaladnis_magnum_opus] + minPower: + affixPool: + + - aspect: [ring_of_red_furor] + minPower: + affixPool: + + - aspect: [tuskhelm_of_joritz_the_mighty] + minPower: + affixPool: + + - aspect: [airidahs_inexorable_will] + minPower: + affixPool: + + - aspect: [dolmen_stone] + minPower: + affixPool: + + - aspect: [fleshrender] + minPower: + affixPool: + + - aspect: [greatstaff_of_the_crone] + minPower: + affixPool: + + - aspect: [hunters_zenith] + minPower: + affixPool: + + - aspect: [insatiable_fury] + minPower: + affixPool: + + - aspect: [mad_wolfs_glee] + minPower: + affixPool: + + - aspect: [storms_companion] + minPower: + affixPool: + + - aspect: [tempest_roar] + minPower: + affixPool: + + - aspect: [vasilys_prayer] + minPower: + affixPool: + + - aspect: [waxing_gibbous] + minPower: + affixPool: + + - aspect: [black_river] + minPower: + affixPool: + + - aspect: [blood_artisans_cuirass] + minPower: + affixPool: + + - aspect: [blood_moon_breeches] + minPower: + affixPool: + + - aspect: [bloodless_scream] + minPower: + affixPool: + + - aspect: [deathless_visage] + minPower: + affixPool: + + - aspect: [deathspeakers_pendant] + minPower: + affixPool: + + - aspect: [greaves_of_the_empty_tomb] + minPower: + affixPool: + + - aspect: [howl_from_below] + minPower: + affixPool: + + - aspect: [lidless_wall] + minPower: + affixPool: + + - aspect: [ring_of_mendeln] + minPower: + affixPool: + + - aspect: [ring_of_sacrilegious_soul] + minPower: + affixPool: + + - aspect: [ashearas_khanjar] + minPower: + affixPool: + + - aspect: [condemnation] + minPower: + affixPool: + + - aspect: [cowl_of_the_nameless] + minPower: + affixPool: + + - aspect: [eaglehorn] + minPower: + affixPool: + + - aspect: [eyes_in_the_dark] + minPower: + affixPool: + + - aspect: [grasp_of_shadow] + minPower: + affixPool: + + - aspect: [scoundrels_leathers] + minPower: + affixPool: + + - aspect: [skyhunter] + minPower: + affixPool: + + - aspect: [windforce] + minPower: + affixPool: + + - aspect: [word_of_hakan] + minPower: + affixPool: + + - aspect: [writhing_band_of_trickery] + minPower: + affixPool: + + - aspect: [blue_rose] + minPower: + affixPool: + + - aspect: [esadoras_overflowing_cameo] + minPower: + affixPool: + + - aspect: [esus_heirloom] + minPower: + affixPool: + + - aspect: [flamescar] + minPower: + affixPool: + + - aspect: [gloves_of_the_illuminator] + minPower: + affixPool: + + - aspect: [iceheart_brais] + minPower: + affixPool: + + - aspect: [rainment_of_the_infinite] + minPower: + affixPool: + + - aspect: [staff_of_endless_rage] + minPower: + affixPool: + + - aspect: [staff_of_lam_esen] + minPower: + affixPool: + + - aspect: [tal_rashas_iridescent_loop] + minPower: + affixPool: + + - aspect: [the_oculus] + minPower: + affixPool: + diff --git a/src/config.py b/src/config.py index 029be8c7..8f1253bf 100644 --- a/src/config.py +++ b/src/config.py @@ -76,7 +76,6 @@ def load_data(self): profiles_str = str(self._select_val("general", "profiles")) self.general = { "check_chest_tabs": int(self._select_val("general", "check_chest_tabs")), - "whitelist_uniques": ast.literal_eval(self._select_val("general", "whitelist_uniques").title()), "run_vision_mode_on_startup": ast.literal_eval(self._select_val("general", "run_vision_mode_on_startup").title()), "hidden_transparency": max(0.01, float(self._select_val("general", "hidden_transparency"))), "local_prefs_path": self._select_val("general", "local_prefs_path"), diff --git a/src/item/corrections.py b/src/item/corrections.py new file mode 100644 index 00000000..86ffc1bc --- /dev/null +++ b/src/item/corrections.py @@ -0,0 +1,137 @@ +# Some aspects/uniques have their variable number as second (number_idx_1) or third (number_idx_2) +ASPECT_NUMBER_AT_IDX1 = [ + # Legendary + "frostbitten_aspect", + "frostbitten_aspect", + "aspect_of_artful_initiative", + "aspect_of_artful_initiative", + "aspect_of_noxious_ice", + "aspect_of_noxious_ice", + "elementalists_aspect", + "elementalists_aspect", + "snowveiled_aspect", + "snowveiled_aspect", + "aspect_of_might", + "aspect_of_might", + "assimilation_aspect", + "assimilation_aspect", + "exploiters_aspect", + "exploiters_aspect", + "aspect_of_audacity", + "aspect_of_audacity", + "ghostwalker_aspect", + "ghostwalker_aspect", + "aspect_of_slaughter", + "aspect_of_slaughter", + "aspect_of_tempering_blows", + "aspect_of_tempering_blows", + "aspect_of_ancestral_charge", + "aspect_of_ancestral_charge", + "aspect_of_encroaching_wrath", + "aspect_of_encroaching_wrath", + "brawlers_aspect", + "brawlers_aspect", + "devilish_aspect", + "devilish_aspect", + "earthstrikers_aspect", + "earthstrikers_aspect", + "steadfast_berserkers_aspect", + "steadfast_berserkers_aspect", + "windlasher_aspect", + "windlasher_aspect", + "bear_clan_berserkers_aspect", + "bear_clan_berserkers_aspect", + "aspect_of_mending_stone", + "aspect_of_mending_stone", + "aspect_of_metamorphic_stone", + "aspect_of_metamorphic_stone", + "aspect_of_the_stampede", + "aspect_of_the_stampede", + "aspect_of_the_trampled_earth", + "aspect_of_the_trampled_earth", + "lightning_dancers_aspect", + "lightning_dancers_aspect", + "raw_might_aspect", + "raw_might_aspect", + "aspect_of_decay", + "aspect_of_decay", + "osseous_gale_aspect", + "osseous_gale_aspect", + "rotting_aspect", + "rotting_aspect", + "aspect_of_exposed_flesh", + "aspect_of_exposed_flesh", + "coldbringers_aspect", + "coldbringers_aspect", + "aspect_of_uncanny_treachery", + "aspect_of_uncanny_treachery", + "aspect_of_lethal_dusk", + "aspect_of_lethal_dusk", + "enshrouding_aspect", + "enshrouding_aspect", + "aspect_of_arrow_storms", + "aspect_of_arrow_storms", + "aspect_of_bursting_venoms", + "aspect_of_bursting_venoms", + "aspect_of_pestilent_points", + "aspect_of_pestilent_points", + "aspect_of_synergy", + "aspect_of_synergy", + "icy_alchemists_aspect", + "icy_alchemists_aspect", + "toxic_alchemists_aspect", + "toxic_alchemists_aspect", + "trickshot_aspect", + "trickshot_aspect", + "snowguards_aspect", + "snowguards_aspect", + "aspect_of_frozen_orbit", + "aspect_of_frozen_orbit", + "serpentine_aspect", + "serpentine_aspect", + # Unique + "banished_lords_talisman", + "ancients_oath", + "battle_trance", + "gohrs_devastating_grips", + "waxing_gibbous", + "black_river", + "bloodless_scream", + "ring_of_mendeln", + "blue_rose", + "esadoras_overflowing_cameo", + "staff_of_endless_rage", + "fists_of_fate", + "mothers_embrace", + "temerity", + "the_butchers_cleaver", + "xfals_corroded_signet", + "writhing_band_of_trickery", + "airidahs_inexorable_will", +] + +ASPECT_NUMBER_AT_IDX2 = [ + # Legendary + "aspect_of_retribution", + "aspect_of_retribution", + "aspect_of_serration", + "aspect_of_serration", + "aspect_of_untimely_death", + "aspect_of_untimely_death", + # Unique + "azurewrath", + "soulbrand", + "ring_of_red_furor", +] + +ERROR_MAP = { + "thoms": "thorns", + "seythe": "scythe", + "@arbarian": "(barbarian", + "mruid": "(druid", + "omuid": "(druid", + "gorcerer": "sorcerer", + "garbarian": "barbarian", + "two-handed!": "two-handed", + "two-handed.": "two-handed", +} diff --git a/src/item/filter.py b/src/item/filter.py index af285972..e63b4def 100644 --- a/src/item/filter.py +++ b/src/item/filter.py @@ -18,6 +18,8 @@ class Filter: affix_dict = json.load(f) with open("assets/aspects.json", "r") as f: aspect_dict = json.load(f) + with open("assets/aspects_unique.json", "r") as f: + aspect_unique_dict = json.load(f) files_loaded = False all_file_pathes = [] last_loaded = None @@ -48,19 +50,17 @@ def check_item_types(filters): Logger.warning(f"Warning: Invalid ItemTypes in filter {filter_name}: {', '.join(invalid_types)}") @staticmethod - def check_affixes(filters, affix_dict): - for filter_dict in filters: - for filter_name, filter_data in filter_dict.items(): - user_affix_pool = [filter_data["affixPool"]] if isinstance(filter_data["affixPool"], str) else filter_data["affixPool"] - invalid_affixes = [] - if user_affix_pool is None: - continue - for affix in user_affix_pool: - affix_name = affix if isinstance(affix, str) else affix[0] - if affix_name not in affix_dict: - invalid_affixes.append(affix_name) - if invalid_affixes: - Logger.warning(f"Warning: Invalid Affixes in filter {filter_name}: {', '.join(invalid_affixes)}") + def check_affix_pool(affix_pool, affix_dict, filter_name): + user_affix_pool = affix_pool + invalid_affixes = [] + if user_affix_pool is None: + return + for affix in user_affix_pool: + affix_name = affix if isinstance(affix, str) else affix[0] + if affix_name not in affix_dict: + invalid_affixes.append(affix_name) + if invalid_affixes: + Logger.warning(f"Warning: Invalid Affixes in filter {filter_name}: {', '.join(invalid_affixes)}") def load_files(self): self.files_loaded = True @@ -111,7 +111,10 @@ def load_files(self): # Sanity check on the item types self.check_item_types(self.affix_filters[profile_str]) # Sanity check on the affixes - self.check_affixes(self.affix_filters[profile_str], self.affix_dict) + for filter_dict in self.affix_filters[profile_str]: + for filter_name, filter_data in filter_dict.items(): + if "affixPool" in filter_data: + self.check_affix_pool(filter_data["affixPool"], self.affix_dict, filter_name) if config is not None and "Aspects" in config: info_str += "Aspects " @@ -133,10 +136,19 @@ def load_files(self): if config["Uniques"] is None: Logger.error(f"Empty Uniques section in {profile_str}. Remove it") return - # Sanity check on the item types - self.check_item_types(self.unique_filters[profile_str]) - # Sanity check on the affixes - self.check_affixes(self.unique_filters[profile_str], self.affix_dict) + # Sanity check for unique aspects + invalid_uniques = [] + for unique in self.unique_filters[profile_str]: + if "aspect" not in unique: + Logger.warning(f"Warning: Unique missing mandatory 'aspect' field in {profile_str} profile") + continue + unique_name = unique["aspect"] if isinstance(unique["aspect"], str) else unique["aspect"][0] + if unique_name not in self.aspect_unique_dict: + invalid_uniques.append(unique_name) + elif "affixPool" in unique: + self.check_affix_pool(unique["affixPool"], self.affix_dict, unique_name) + if invalid_uniques: + Logger.warning(f"Warning: Invalid Unique: {', '.join(invalid_uniques)}") Logger.info(info_str) @@ -151,6 +163,45 @@ def _did_files_change(self) -> bool: return True return False + def _check_power(self, filter_data: dict, item: Item) -> bool: + filter_min_power = filter_data["minPower"] if "minPower" in filter_data else None + item_power_ok = item.power is None or filter_min_power is None or item.power > filter_min_power + return item_power_ok + + def _check_item_type(self, filter_data: dict, item: Item) -> bool: + if "itemType" not in filter_data or filter_data["itemType"] is None: + filter_item_types = None + else: + filter_item_types = [filter_data["itemType"]] if isinstance(filter_data["itemType"], str) else filter_data["itemType"] + item_type_ok = item.type is None or filter_item_types is None or item.type.value in filter_item_types + return item_type_ok + + def _match_affixes(self, filter_data: dict, item: Item) -> list: + if "affixPool" not in filter_data or filter_data["affixPool"] is None: + filter_affix_pool = [] + else: + filter_affix_pool = [filter_data["affixPool"]] if isinstance(filter_data["affixPool"], str) else filter_data["affixPool"] + + matched_affixes = [] + if filter_affix_pool is not None: + for affix in filter_affix_pool: + name, *rest = affix if isinstance(affix, list) else [affix] + threshold = rest[0] if rest else None + condition = rest[1] if len(rest) > 1 else "larger" + + item_affix_value = next((a.value for a in item.affixes if a.type == name), None) + + if item_affix_value is not None: + if ( + threshold is None + or (condition == "larger" and item_affix_value >= threshold) + or (condition == "smaller" and item_affix_value <= threshold) + ): + matched_affixes.append(name) + elif any(a.type == name for a in item.affixes): + matched_affixes.append(name) + return matched_affixes + def should_keep(self, item: Item) -> tuple[bool, bool, list[str], str]: if not self.files_loaded or self._did_files_change(): self.load_files() @@ -158,131 +209,64 @@ def should_keep(self, item: Item) -> tuple[bool, bool, list[str], str]: if item.type is None or item.power is None: return False, False, [], "" + # Filter Magic, Rare, Legendary if item.rarity != ItemRarity.Unique: for profile_str, affix_filter in self.affix_filters.items(): for filter_dict in affix_filter: for filter_name, filter_data in filter_dict.items(): - filter_item_types = ( - [filter_data["itemType"]] if isinstance(filter_data["itemType"], str) else filter_data["itemType"] - ) - filter_min_power = filter_data["minPower"] - filter_affix_pool = ( - [filter_data["affixPool"]] if isinstance(filter_data["affixPool"], str) else filter_data["affixPool"] - ) filter_min_affix_count = filter_data["minAffixCount"] - - if item.type.value not in filter_item_types or ( - item.power is not None and filter_min_power is not None and item.power < filter_min_power - ): + power_ok = self._check_power(filter_data, item) + type_ok = self._check_item_type(filter_data, item) + if not power_ok or not type_ok: continue - - matched_affixes = [] - if filter_affix_pool is not None: - for affix in filter_affix_pool: - name, *rest = affix if isinstance(affix, list) else [affix] - threshold = rest[0] if rest else None - condition = rest[1] if len(rest) > 1 else "larger" - - item_affix_value = next((a.value for a in item.affixes if a.type == name), None) - - if item_affix_value is not None: - if ( - threshold is None - or (condition == "larger" and item_affix_value >= threshold) - or (condition == "smaller" and item_affix_value <= threshold) - ): - matched_affixes.append(name) - elif any(a.type == name for a in item.affixes): - matched_affixes.append(name) - + matched_affixes = self._match_affixes(filter_data, item) if filter_min_affix_count is None or len(matched_affixes) >= filter_min_affix_count: affix_debug_msg = [name for name in matched_affixes] Logger.info(f"Matched {profile_str}.{filter_name}: {affix_debug_msg}") return True, True, matched_affixes, f"{profile_str}.{filter_name}" - if item.aspect and item.rarity != ItemRarity.Unique: - for profile_str, aspect_filter in self.aspect_filters.items(): - for filter_data in aspect_filter: - aspect_name, *rest = filter_data if isinstance(filter_data, list) else [filter_data] + if item.aspect: + for profile_str, aspect_filter in self.aspect_filters.items(): + for filter_data in aspect_filter: + aspect_name, *rest = filter_data if isinstance(filter_data, list) else [filter_data] + threshold = rest[0] if rest else None + condition = rest[1] if len(rest) > 1 else "larger" + + if item.aspect.type == aspect_name: + if ( + threshold is None + or item.aspect.value is None + or (condition == "larger" and item.aspect.value >= threshold) + or (condition == "smaller" and item.aspect.value <= threshold) + ): + Logger.info(f"Matched {profile_str}.Aspects: [{item.aspect.type}, {item.aspect.value}]") + return True, False, [], f"{profile_str}.Aspects" + + # Filter Uniques + if item.rarity == ItemRarity.Unique: + for profile_str, unique_filter in self.unique_filters.items(): + for filter_dict in unique_filter: + unique_name, *rest = filter_dict["aspect"] if isinstance(filter_dict["aspect"], list) else [filter_dict["aspect"]] threshold = rest[0] if rest else None condition = rest[1] if len(rest) > 1 else "larger" - if item.aspect.type == aspect_name: + if item.aspect.type == unique_name: if ( threshold is None or item.aspect.value is None or (condition == "larger" and item.aspect.value >= threshold) or (condition == "smaller" and item.aspect.value <= threshold) ): - Logger.info(f"Matched {profile_str}.Aspects: [{item.aspect.type}, {item.aspect.value}]") - return True, False, [], f"{profile_str}.Aspects" - - if item.rarity == ItemRarity.Unique: - for profile_str, unique_filter in self.unique_filters.items(): - for filter_dict in unique_filter: - for filter_name, filter_data in filter_dict.items(): - filter_item_types = filter_data["itemType"] - filter_min_power = filter_data["minPower"] - filter_affix_pool = ( - [filter_data["affixPool"]] if isinstance(filter_data["affixPool"], str) else filter_data["affixPool"] - ) - filter_min_affix_count = len(filter_affix_pool) # all have to match - - matched_affixes = [] - found_affixes = [] - if filter_affix_pool is not None: - for affix in filter_affix_pool: - name, *rest = affix if isinstance(affix, list) else [affix] - threshold = rest[0] if rest else None - condition = rest[1] if len(rest) > 1 else "larger" - - item_affix_value = next((a.value for a in item.affixes if a.type == name), None) - - if any(a.type == name for a in item.affixes): - found_affixes.append(name) - - if item_affix_value is not None: - if ( - threshold is None - or (condition == "larger" and item_affix_value >= threshold) - or (condition == "smaller" and item_affix_value <= threshold) - ): - matched_affixes.append(name) - elif any(a.type == name for a in item.affixes): - matched_affixes.append(name) - - item_type_ok = item.type.value in filter_item_types - item_power_ok = item.power is None or filter_min_power is None or item.power >= filter_min_power - if ( - (filter_min_affix_count is None or len(matched_affixes) >= filter_min_affix_count) - and item_type_ok - and item_power_ok - ): - av = filter_data["aspectValue"] if "aspectValue" in filter_data else None - if av is not None: - rest = filter_data if isinstance(filter_data["aspectValue"], list) else [filter_data["aspectValue"]] - threshold = rest[0] if rest else None - condition = rest[1] if len(rest) > 1 else "larger" - - aspect_ok = ( - item.aspect is None - or item.aspect.value is None - or threshold is None - or (condition == "larger" and item.aspect.value >= threshold) - or (condition == "smaller" and item.aspect.value <= threshold) - ) - - if aspect_ok: - Logger.info(f"Matched {profile_str}.{filter_name}") - return True, True, [], f"{profile_str}.{filter_name}" - else: - return True, True, [], f"{profile_str}.{filter_name}" - - if filter_min_affix_count is not None and len(found_affixes) >= filter_min_affix_count: - return False, False, [], f"{profile_str}.{filter_name}" - - if not Config().general["whitelist_uniques"]: - # We didnt find any match to that unique, if whitelist is false we return True - return True, True, [], "unique.no_config" + # CHeck Affixes + for filter_name, filter_data in filter_dict.items(): + filter_affix_pool = [] if "affixPool" not in filter_data else filter_data["affixPool"] + filter_min_affix_count = len(filter_affix_pool) + power_ok = self._check_power(filter_data, item) + if not power_ok: + continue + matched_affixes = self._match_affixes(filter_data, item) + if filter_min_affix_count is None or len(matched_affixes) >= filter_min_affix_count: + Logger.info(f"Matched {profile_str}.Unique: [{item.aspect.type}, {item.aspect.value}]") + return True, True, [], f"{profile_str}.{item.aspect.type}" return False, False, [], "" diff --git a/src/item/read_descr.py b/src/item/read_descr.py index 5816bbb9..dfa12a90 100644 --- a/src/item/read_descr.py +++ b/src/item/read_descr.py @@ -7,6 +7,7 @@ from item.data.affix import Affix from item.data.aspect import Aspect from item.models import Item +from item.corrections import ASPECT_NUMBER_AT_IDX1, ASPECT_NUMBER_AT_IDX2, ERROR_MAP from template_finder import search from utils.ocr.read import image_to_text from utils.image_operations import crop, color_filter @@ -16,19 +17,6 @@ from rapidfuzz import process from config import Config - -ERROR_MAP = { - "thoms": "thorns", - "seythe": "scythe", - "@arbarian": "(barbarian", - "mruid": "(druid", - "omuid": "(druid", - "gorcerer": "sorcerer", - "garbarian": "barbarian", - "two-handed!": "two-handed", - "two-handed.": "two-handed", -} - affix_dict = dict() with open("assets/affixes.json", "r") as f: affix_dict = json.load(f) @@ -37,6 +25,10 @@ with open("assets/aspects.json", "r") as f: aspect_dict = json.load(f) +aspect_unique_dict = dict() +with open("assets/aspects_unique.json", "r") as f: + aspect_unique_dict = json.load(f) + def _closest_match(target, candidates, min_score=84): keys, values = zip(*candidates.items()) @@ -228,6 +220,10 @@ def read_descr(rarity: ItemRarity, img_item_descr: np.ndarray, show_warnings: bo cropped_bottom = crop(img_item_descr, roi_bottom) unique_filtered, _ = color_filter(cropped_bottom, Config().colors["unique_gold"], False) unique_aspect_row = affix_bullets.matches[0].center[1] + np.any(unique_filtered != 0, axis=1).argmax() + unique_aspect_row_end = ( + affix_bullets.matches[0].center[1] + len(unique_filtered) - np.any(unique_filtered[::-1] != 0, axis=1).argmax() - 1 + ) + # print("Runtime (start_tex_2): ", time.time() - start_tex_2) # Affixes @@ -303,9 +299,10 @@ def read_descr(rarity: ItemRarity, img_item_descr: np.ndarray, show_warnings: bo if rarity in [ItemRarity.Legendary, ItemRarity.Unique]: if rarity == ItemRarity.Legendary: ab = aspect_bullets.matches[0].center + bottom_limit = empty_sockets.matches[0].center[1] if len(empty_sockets.matches) > 0 else img_height else: ab = [unique_offset_x, unique_aspect_row + int(line_height / 3)] - bottom_limit = empty_sockets.matches[0].center[1] if len(empty_sockets.matches) > 0 else img_height + bottom_limit = unique_aspect_row_end dy = bottom_limit - ab[1] dx_offset = line_height // 4 dy_offset = int(line_height * 0.7) @@ -315,21 +312,18 @@ def read_descr(rarity: ItemRarity, img_item_descr: np.ndarray, show_warnings: bo concatenated_str = image_to_text(img_full_aspect).text.lower().replace("\n", " ") cleaned_str = _clean_str(concatenated_str) - found_key = None if rarity == ItemRarity.Legendary: found_key = _closest_match(cleaned_str, aspect_dict, min_score=77) + else: + found_key = _closest_match(cleaned_str, aspect_unique_dict, min_score=77) - # Filter for the "blue number" - mask, _ = color_filter(img_full_aspect, Config().colors[f"aspect_number"], False) - if Config().ui_pos["window_dimensions"][1] == 2160: - kernel = np.ones((4, 4), np.uint8) - elif Config().ui_pos["window_dimensions"][1] == 1440: - kernel = np.ones((3, 3), np.uint8) + if found_key in ASPECT_NUMBER_AT_IDX1: + idx = 1 + elif found_key in ASPECT_NUMBER_AT_IDX2: + idx = 2 else: - kernel = np.ones((2, 2), np.uint8) - mask = cv2.dilate(mask, kernel, iterations=1) - value_str = image_to_text(mask, spares_text=True).text.lower().replace("\n", " ") - found_value = _find_number(value_str) + idx = 0 + found_value = _find_number(concatenated_str, idx) # Scale the aspect down to the canonical range if found on an item that scales it up if found_value is not None and rarity == ItemRarity.Legendary: @@ -348,16 +342,17 @@ def read_descr(rarity: ItemRarity, img_item_descr: np.ndarray, show_warnings: bo ]: found_value /= 2 - if found_key is not None or rarity == ItemRarity.Unique: - # Rapid detects 19 as 199 + Logger.debug(f"{found_key}: {found_value}") + if found_key is not None: + # Rapid detects 19 as 199 often if found_key == "rapid_aspect" and found_value == 199: found_value = 19 aspect_loc = [ab[0], ab[1]] item.aspect = Aspect(found_key, found_value, concatenated_str, aspect_loc) else: if show_warnings: - Logger.warning(f"Could not find aspect: {cleaned_str}") - screenshot("failed_aspect", img=img_item_descr) + Logger.warning(f"Could not find aspect/unique: {cleaned_str}") + screenshot("failed_aspect_or_unique", img=img_item_descr) return None # print("Runtime (start_aspect): ", time.time() - start_aspect) diff --git a/src/scripts/rogue_tb.py b/src/scripts/rogue_tb.py index 6a9c3723..0fcd2061 100644 --- a/src/scripts/rogue_tb.py +++ b/src/scripts/rogue_tb.py @@ -15,16 +15,11 @@ def run_rogue_tb(): img = Cam().grab() if hud.is_ingame(img): ready4 = hud.is_skill_ready(img) - ready3 = hud.is_skill_ready(img, "skill3") imbued = hud.is_imbued(img) - # Logger.debug(f"imbued: {imbued} s3: {ready3} s4: {ready4}") if not imbued: if ready4: keyboard.send(Config().char["skill4"]) Logger.debug("Casting imbuement (skill4)") - elif ready3: - keyboard.send(Config().char["skill3"]) - Logger.debug("Casting imbuement (skill3)") wait(0.1, 0.15) diff --git a/src/tools/adapt_json.py b/src/tools/adapt_json.py new file mode 100644 index 00000000..509461dd --- /dev/null +++ b/src/tools/adapt_json.py @@ -0,0 +1,43 @@ +import json +import re + +# Step 1: Read the JSON file +with open("assets/aspects_unique.json", "r") as file: + data = json.load(file) + + +def _remove_text_after_first_keyword(text, keywords): + for keyword in keywords: + match = re.search(re.escape(keyword), text) + if match: + return text[: match.start()] + return text + + +def _clean_str(s): + cleaned_str = re.sub(r"(\+)?\d+(\.\d+)?%?", "", s) # Remove numbers and trailing % or preceding + + cleaned_str = re.sub(r"[\[\]+\-:%\']", "", cleaned_str) # Remove [ and ] and leftover +, -, %, :, ' + cleaned_str = re.sub( + r"\((rogue|barbarian|druid|sorcerer|necromancer) only\)", "", cleaned_str + ) # this is not included in our affix table + cleaned_str = _remove_text_after_first_keyword(cleaned_str, ["requires level", "requires lev", "account", "sell value"]) + cleaned_str = re.sub( + r"(scroll up|account bound|requires level|only\)|sell value|durability|barbarian|rogue|sorceress|druid|necromancer|not useable|by your class|by your clas)", + "", + cleaned_str, + ) # Remove new terms + cleaned_str = " ".join(cleaned_str.split()) # Remove extra spaces + return cleaned_str + + +# Step 2: Process the data (example: modifying keys and values) +# Modify this part according to your specific needs +modified_data = dict() +for key, value in data.items(): + new_key = key.strip().lower().replace(" ", "_").replace("'", "") + new_val = _clean_str(value.lower()) + modified_data[new_key] = new_val + +# Step 3: Write to a new JSON file +with open("new_data.json", "w") as file: + json.dump(modified_data, file, indent=4) diff --git a/src/utils/ocr/read.py b/src/utils/ocr/read.py index fd2e3b1a..8371ad21 100644 --- a/src/utils/ocr/read.py +++ b/src/utils/ocr/read.py @@ -24,7 +24,6 @@ # bypassing hacks that are Tesseract-specif API = PyTessBaseAPI(psm=3, oem=OEM.LSTM_ONLY, path=TESSDATA_PATH, lang="eng") -API2 = PyTessBaseAPI(psm=12, oem=OEM.LSTM_ONLY, path=TESSDATA_PATH, lang="eng") # supposed to give fruther runtime improvements, but reading performance really goes down... # API.SetVariable("tessedit_do_invert", "0") @@ -62,7 +61,7 @@ def _strip_new_lines(original_text: str, psm: int) -> str: return new_text -def image_to_text(img: np.ndarray, spares_text: bool = False) -> OcrResult: +def image_to_text(img: np.ndarray) -> OcrResult: """ Extract text from the entire image. :param img: The input image. @@ -72,12 +71,8 @@ def image_to_text(img: np.ndarray, spares_text: bool = False) -> OcrResult: :return: The OCR result. """ psm = 3 - if spares_text: - API2.SetImageBytes(*_img_to_bytes(img)) - original_text = API2.GetUTF8Text().strip() - else: - API.SetImageBytes(*_img_to_bytes(img)) - original_text = API.GetUTF8Text().strip() + API.SetImageBytes(*_img_to_bytes(img)) + original_text = API.GetUTF8Text().strip() text = _strip_new_lines(original_text, psm) res = OcrResult( original_text=original_text, diff --git a/test/item/read_descr_test.py b/test/item/read_descr_test.py index 69c2b197..73f42e16 100644 --- a/test/item/read_descr_test.py +++ b/test/item/read_descr_test.py @@ -125,7 +125,7 @@ ItemRarity.Unique, ItemType.Dagger, 892, - Aspect(None, 22), + Aspect("condemnation", 22), [ Affix("basic_skill_attack_speed", 5.6), Affix("critical_strike_damage", 20.5),