|
import configparser |
|
import json |
|
import os.path |
|
|
|
import numpy as np |
|
import pulp as pl |
|
|
|
#################################################### |
|
# THEORETICAL Stat Optimizer by Mijago |
|
# - You tell the script which stats you want (DESIRED_STATS) |
|
# - Further fine-tune the configuration |
|
# - Add d2ap_armor.json (from D2ArmorPicker -> Account) |
|
# This script is able to tell you if you are able to reach a desired stat distribution, and which pieces you need. |
|
# If you are not able to reach the distribution you want, it can tell you which armor pieces you can use |
|
# and also the armor pieces you theoretically have to farm. |
|
# Note that this script only returns one of potentially many possible results. |
|
|
|
# Random thoughts and bits of information that I do not want to forget. |
|
# - Feel free to play around with the grading weights. It impacts the results drastically and is unique to every problem. |
|
# - The tool can select major and minor mods. We could give major mods a penalty so that the algorithm should prefer |
|
# the usage of minor mods. BUT we already heavily penalize wasted stats. Due to this, the algorithm will always use |
|
# a minor mod whenever possible. If we'd add the penalty, however, then it could also give us three minor mods in some |
|
# rare cases, which could be interesting for some. You can add the following to the weights: |
|
# - 1 * pl.lpSum(v_mods_major) - 0.5 * pl.lpSum(v_mods_minor) |
|
# - Since the newest version it can also select the following: |
|
# - class (some have more fragment slots, but you can specify this to your desired class) |
|
# - subclass (required for the aspects and fragment selection) |
|
# - aspects (only really required to get the amount of fragment slots) |
|
# - fragments (you can fix the hashes of those you definitely need, can be disabled) |
|
# - For very complex things, like "47 tiers", you have to disable some features that make the model very complex. |
|
# For example, set stats.max_value to -1, and increase the timelimit_seconds. |
|
|
|
###################################### |
|
# INPUT |
|
# Set this to the name of the config file you want to read. |
|
READ_CONFIG_FROM_FILE: None or str = None # "example.cfg" |
|
config = dict( |
|
main=dict( |
|
# Set your desired stats. Be realistic, please |
|
# The order is: [mob, res, rec, dis, int, str] |
|
desired_stats=[0, 0, 0, 100, 0, 0], |
|
# Set this to true if you want EXACT stats. Yes, that means you can ask the tool to get 6x69 stats.. |
|
desired_stats_exact=False, |
|
# Set the amount of stat mods you want to use. |
|
available_mods=5, |
|
# static_stat_bonus represents your stat mods (Powerful Friends, Radiant Light) that apply a |
|
# bonus or a penalty. You also add your desired subclass fragments in here, if they provide a bonus. |
|
# This value is always added to the baseline of the stats. You can also add values to theorize. |
|
static_stat_bonus=[0, 0, 0, 0, 0, 0], |
|
# Set the class of the character. This is important for two things: |
|
# First, the selection of the armor from d2ap_armor.json that is being used. |
|
# Second, the selection of possible intrinsic stats for the generator of theoretical armor. |
|
# You can also select "-1" if you want the tool to figure it out. |
|
# -1 to let the script decide, or 0: Titan; 1: Hunter; 2: Warlock |
|
character_class=-1, |
|
), |
|
stats=dict( |
|
# Upper limit for stats. Use this to not waste too many points over 100 |
|
# Set it to -1 to disable this limit. This is required for some crazy things like 47 tiers. |
|
max_value=109, |
|
# The upper limit for wasted stats. |
|
# Set it to -1 to disable this limit. |
|
max_waste=-1, |
|
# The minimum amount of stat tiers. A tier is a 10-step of a stat. |
|
min_tiers=32, |
|
# The minimum amount of stats at T10 or higher |
|
min_100_stats=0 |
|
), |
|
weights=dict( |
|
# !!! Warning !!! |
|
# If you have VERY intense boundaries, like "47 tiers", set all of these to 0. |
|
# They drastically increase the complexity and runtime and are not required for extreme cases. |
|
|
|
# Set this to 20 or a bit higher to enforce builds with lower or zero waste.W |
|
waste_penalty=0, |
|
# If you want to penalize mods (i.e. reduce the required amount), increase this |
|
stat_mod_penalty=1, |
|
# If you want to penalize fragment slots (i.e. reduce the required amount), increase this |
|
fragment_slot_penalty=10, |
|
# If you want to penalize fragments (i.e. reduce the required amount), increase this |
|
fragment_penalty=10, |
|
# Here you can set weights for each of the six stats. |
|
# If you increase this, then the tool will try to put this stat in the foreground, |
|
# AFTER all your requirements have been resolved. |
|
# I'd recommend to set the one you really want to 1, the next one to 0.75, then 0.5 etc. |
|
# Usually you can leave them all at 0. This is only for stat-preference. |
|
# But in the end it comes down to your selected stats and what you want. |
|
# You can also add a stat penalty here - simply give it a negative value around -5 to -10. |
|
# Setting weights.intellect to -10 would basically tell the tool to never use intellect. |
|
mobility=0, |
|
resilience=0, |
|
recovery=0, |
|
discipline=0, |
|
intellect=0, |
|
strength=0 |
|
), |
|
own_armor=dict( |
|
# Set select_own_armor=True to see if you can reach your desired distribution with your own armor. |
|
# Set select_own_armor=False to only generate theoretical armor. |
|
# If you disable this, you do not need to present d2ap_armor.json to the tool. |
|
# If the tool can not find d2ap_armor.json, then it'll automatically set USE_OWN_ARMOR=False. |
|
select_own_armor=True, |
|
# Set this to true if you want it to have an exotic |
|
require_exotic=False, |
|
# Add armor itemInstanceId you want to ignore. Must be a number |
|
# You can find this in the "Armor Investigation" tab in D2ArmorPicker, or in the results of this tool. |
|
# You can also find this in the d2ap_armor.json |
|
ignored_armor_ids=[ |
|
# 6917529303135369161 |
|
], |
|
# If you want a specific item in a specific slot, you can add the hash in here. |
|
# You can specify as many as you want. Slot order: Helmet, Gauntlet, Chest, Legs |
|
# If an entry is empty, it accepts every item. |
|
# Example line to enforce Ashen Wake: (You get this ID from the light.gg URL) |
|
# <https://www.light.gg/db/items/1734844650/ashen-wake/> turns into |
|
# [1734844650], # gauntlet |
|
# TODO: It currently does NOT take this into account in the generation process of theoretical armor. |
|
# That means, it will not know when NOT to use exotic intrinsic stats when generating armor. |
|
# You usually will not run into a situation where this occurs, but it is still important to know. |
|
required_armor_hashes=[ |
|
[], # helmet |
|
[], # gauntlet |
|
[], # chest |
|
[], # legs |
|
] |
|
), |
|
theoretical_armor_pieces=dict( |
|
# Settings for the generation of theoretical items |
|
# If you set generate_theoretical_if_impossible=False then it will only use the armor from d2ap_armor.json |
|
# It is NOT possible to set select_own_armor=False and emulate_theoretical_armor=False at the same time |
|
emulate_theoretical_armor=True, |
|
# Exotics that were introduced pre Shadowkeep have so-called Intrinsic stats. |
|
# They are only in mob/res/rec, but may help sometimes. |
|
# Setting emulate_exotics_w_intrinsic_stats=True allows the generator for theoretical |
|
# armor to utilize these. |
|
emulate_exotics_w_intrinsic_stats=True, |
|
# Use this setting to disable intrinsic stats in specific slots. |
|
# 1 means active, 0 means off. [Helmet, Gauntlet, Chest, Legs] |
|
emulate_exotics_w_intrinsic_stats_slots=[1, 1, 1, 1], |
|
# Setting this to True means that the script will always use the armor you set in REQUIRED_ARMOR_HASHES, even if this |
|
# will lead to a failure (due to the stat distribution not being reachable). |
|
# Setting this to False means that it will generate theoretical armor pieces in these slots that you need to grind, |
|
# which can be useful if you want to reach a certain distribution with a specific exotic armor. |
|
enforce_required_armor_pieces=True |
|
), |
|
combat_style_mod_selection=dict( |
|
# This allows the tool to add Radiant Light and Powerful Friends |
|
# Disable this if you want to use static_stat_bonus instead |
|
enable_combatstyle_mod_selection=True, |
|
), |
|
fragment_selection=dict( |
|
# If True, then the generator will pick class, subclass, aspects and fragments to fit your stats. |
|
# You can set ARMOR_CLASS = -1 for this, and it will also select the best class. |
|
enable_fragment_selection=True, |
|
# If you want a predefined set of fragments, add them in here. |
|
required_fragment_hashes=[], |
|
# If you want to ignore a fragment, add it in here |
|
ignored_fragment_hashes=[ |
|
# Negative ARC |
|
# 1727069364, 1727069360, 1727069362, |
|
# Negative SOLAR |
|
# 362132294, 362132290, 362132292, |
|
# Negative VOID |
|
# 2661180603, 2661180601, 2272984668, 2272984671, 2272984664, |
|
# Negative STASIS |
|
# 2483898431, 3469412970, 537774542, 3469412974 |
|
], |
|
# You can set the subclass that you want the generator to use. |
|
# -1 to let the script decide, otherwise: 0: arc; 1: solar; 2: void; 3: stasis |
|
selected_subclass=-1, |
|
), |
|
model=dict( |
|
timelimit_seconds=20, |
|
print_debug_messages=False |
|
) |
|
) |
|
|
|
###################################### |
|
# reading config and applying it to the lookup variables |
|
# DO NOT MODIFY ANYTHING IN THIS SECTION OR BELOW IT EXCEPT IF YOU KNOW WHAT YOU ARE DOING |
|
parser = configparser.ConfigParser() |
|
if READ_CONFIG_FROM_FILE is not None: |
|
parser.read(READ_CONFIG_FROM_FILE) |
|
else: |
|
parser.read_dict(config) |
|
# Writing our configuration file to 'example.cfg' |
|
with open('recent.cfg', 'w') as configfile: |
|
parser.write(configfile) |
|
|
|
DESIRED_STATS = parser._get_conv("main", "desired_stats", lambda d: json.loads(d), fallback=[10, 10, 100, 10, 10, 10]) |
|
DESIRED_STATS_EXACT = parser.getboolean("main", "desired_stats_exact", fallback=False) |
|
AVAILABLE_MODS = parser.getint("main", "available_mods", fallback=5) |
|
MINIMUM_TIERS = parser.getint("stats", "min_tiers", fallback=28) |
|
MIN_100_STATS = parser.getint("stats", "min_100_stats", fallback=0) |
|
MAXIMUM_STAT_VALUE = parser.getint("stats", "max_value", fallback=109) |
|
MAXIMUM_STAT_WASTE = parser.getint("stats", "max_waste", fallback=-1) |
|
WASTE_PENALTY_WEIGHT = parser.getfloat("weights", "waste_penalty", fallback=0) |
|
STAT_MOD_PENALTY_WEIGHT = parser.getfloat("weights", "stat_mod_penalty", fallback=0) |
|
FRAGMENT_SLOT_PENALTY_WEIGHT = parser.getfloat("weights", "fragment_slot_penalty", fallback=0) |
|
FRAGMENT_PENALTY_WEIGHT = parser.getfloat("weights", "fragment_penalty", fallback=0) |
|
AVAILABLE_BONUS = parser._get_conv("main", "static_stat_bonus", lambda d: json.loads(d), fallback=[0, 0, 0, 0, 0, 0]) |
|
REQUIRE_EXOTIC = parser.getboolean("own_armor", "require_exotic", fallback=True) |
|
USE_OWN_ARMOR = parser.getboolean("own_armor", "select_own_armor", fallback=True) |
|
ARMOR_CLASS = parser.getint("main", "character_class", fallback=-1) |
|
|
|
GENERATOR_ENABLED = parser.getboolean("theoretical_armor_pieces", "emulate_theoretical_armor", fallback=True) |
|
GENERATOR_USE_EXOTIC_INTRINSICS = parser.getboolean("theoretical_armor_pieces", |
|
"emulate_exotics_w_intrinsic_stats", |
|
fallback=True) |
|
GENERATOR_USE_EXOTIC_INTRINSICS_SLOTS = parser._get_conv("theoretical_armor_pieces", "emulate_exotics_w_intrinsic_stats_slots", lambda d: json.loads(d), fallback=[True, True, True, True]) |
|
GENERATOR_ENABLE_FRAGMENT_SELECTION = parser.getboolean("fragment_selection", |
|
"enable_fragment_selection", |
|
fallback=True) |
|
GENERATOR_SELECTED_SUBCLASS = parser.getint("fragment_selection", "selected_subclass", fallback=-1) |
|
|
|
GENERATOR_ENABLE_COMBATSTYLE_SELECTION = parser.getboolean("combat_style_mod_selection", |
|
"enable_combatstyle_mod_selection", |
|
fallback=True) |
|
|
|
MODEL_TIMELIMIT_SECONDS = parser.getint("model", "timelimit_seconds", fallback=10) |
|
MODEL_PRINT_DEBUG_MESSAGES = parser.getboolean("model", "print_debug_messages", fallback=False) |
|
IGNORED_ARMOR = parser._get_conv("own_armor", "ignored_armor_ids", lambda d: json.loads(d), fallback=[]) |
|
STAT_WEIGHT_MOBILITY = parser.getfloat("weights", "mobility", fallback=0) |
|
STAT_WEIGHT_RESILIENCE = parser.getfloat("weights", "resilience", fallback=0) |
|
STAT_WEIGHT_RECOVERY = parser.getfloat("weights", "recovery", fallback=0) |
|
STAT_WEIGHT_DISCIPLINE = parser.getfloat("weights", "discipline", fallback=0) |
|
STAT_WEIGHT_INTELLECT = parser.getfloat("weights", "intellect", fallback=0) |
|
STAT_WEIGHT_STRENGTH = parser.getfloat("weights", "strength", fallback=0) |
|
|
|
REQUIRED_ARMOR_HASHES = parser._get_conv("own_armor", "required_armor_hashes", lambda d: json.loads(d), |
|
fallback=[[], [], [], []]) |
|
REQUIRED_FRAGMENT_HASHES = parser._get_conv("fragment_selection", "required_fragment_hashes", lambda d: json.loads(d), |
|
fallback=[]) |
|
IGNORED_FRAGMENT_HASHES = parser._get_conv("fragment_selection", "ignored_fragment_hashes", lambda d: json.loads(d), |
|
fallback=[]) |
|
REQUIRED_ARMOR_ENFORCED = parser.getboolean("theoretical_armor_pieces", "enforce_required_armor_pieces", fallback=True) |
|
|
|
###################################### |
|
# Fixed parameters for the algorithm |
|
# DO NOT MODIFY ANYTHING IN THIS SECTION OR BELOW IT EXCEPT IF YOU KNOW WHAT YOU ARE DOING |
|
|
|
# Contains the stat multipliers for each tier. This can be used to rate the score of stat tiers. |
|
# In combination with the individual stat weights we can use this to force the tool to get higher |
|
# tiers in certain stat tiers, lower in others, with the extra information of "how much is this tier |
|
# helping?" |
|
# Clipped to be in the range [0,10] |
|
# TODO: This is not used *yet*. |
|
WEIGHT_STAT_TIER_MULTIPLIERS = True |
|
statTierMultipliers = [ |
|
[1.427, 1.256, 1.11, 1.0, 0.91461, 0.829, 0.7683, 0.72, 0.622, 0.561, 0.5], |
|
[1.427, 1.256, 1.11, 1.0, 0.91461, 0.829, 0.7683, 0.72, 0.622, 0.561, 0.5], |
|
[1.427, 1.256, 1.11, 1.0, 0.91461, 0.829, 0.7683, 0.72, 0.622, 0.561, 0.5], |
|
[1.25, 1.14, 1.04, 1.0, 0.83, 0.72, 0.62, 0.55, 0.50, 0.455, 0.39], |
|
[1.44, 1.276, 1.144, 1.0, 0.9488, 0.9024, 0.8608, 0.8224, 0.798, 0.776, 0.7664], |
|
[1.25, 1.14, 1.04, 1.0, 0.83, 0.72, 0.62, 0.55, 0.50, 0.455, 0.39], |
|
] |
|
|
|
# Todo: add negative ones too |
|
combatStyleMods = [ |
|
[(0, 20), "Powerful Friends", 1484685887], |
|
[(5, 20), "Radiant Light", 2979815167], |
|
] |
|
|
|
subclassHashes = [ |
|
[2932390016, 2550323932, 2842471112, 613647804], |
|
[2328211300, 2240888816, 2453351420, 873720784], |
|
[3168997075, 3941205951, 2849050827, 3291545503], |
|
] |
|
|
|
subclassAspects = [ |
|
[ # Arc |
|
# Titan |
|
[(2, "Touch of Thunder", 1656549672), (2, "Knockout", 1656549674), (1, "Juggernaut", 1656549673)], |
|
# Hunter |
|
[(2, "Flow State", 4194622036), (2, "Tempest Strike", 4194622037), (2, "Lethal Current", 4194622038)], |
|
# Warlock |
|
[(2, "Arc Soul", 1293395731), (2, "Lightning Surge", 1293395730), (2, "Electrostatic Mind", 1293395729)], |
|
], |
|
[ # Solar |
|
# Titan |
|
[(2, "Sol Invictus", 2984351205), (2, "Roaring Flames", 2984351204), (2, "Consecration", 2984351206)], |
|
# Hunter |
|
[(3, "On Your Mark", 3066103999), (2, "Knock 'Em Down", 3066103998), (1, "Gunpowder Gamble", 3066103996)], |
|
# Warlock |
|
[(2, "Heat Rises", 83039194), (2, "Icarus Dash", 83039195), (2, "Touch Of Flame", 83039193)] |
|
], |
|
[ # Void |
|
# Titan |
|
[(2, "Offensive Bulwark", 1602994570), (2, "Controlled Demolition", 1602994568), (1, "Bastion", 1602994569)], |
|
# Hunter |
|
[(2, "Vanishing Step", 187655373), (2, "Stylish Executioner", 187655374), (1, "Trapper's Ambush", 187655372)], |
|
# Warlock |
|
[(2, "Child Of The Old Gods", 2321824287), (2, "Feed The Void", 2321824284), |
|
(1, "Chaos Accelerant", 2321824285)] |
|
], |
|
[ # Stasis |
|
# Titan |
|
[(2, "Tectonic Harvest", 2031919264), (2, "Howl Of The Storm", 1563930741), |
|
(1, "Cryoclasm", 2031919265), (3, "Diamond Lance", 3866705246)], |
|
# Hunter |
|
[(2, "Touch Of Winter", 4184589900), (2, "Grim Harvest", 1920417385), |
|
(1, "Shatterdive", 2934767476), (1, "Winter's Shroud", 2934767477)], |
|
# Warlock |
|
[(2, "Iceflare Bolts", 668903196), (2, "Glacial Harvest", 2651551055), |
|
(2, "Bleak Watcher", 2642597904), (2, "Frostpulse", 668903197)] |
|
] |
|
] |
|
subclassFragments = [ |
|
[ # Arc |
|
([(3, -10)], "Spark of Shock", 1727069364), ([(5, 10)], "Spark of Resistance", 1727069366), |
|
([(2, 10)], "Spark of Volts", 3277705904), ([(6, -10)], "Spark of Focus", 1727069360), |
|
([(5, -10)], "Spark of Discharge", 1727069362), ([(1, 10)], "Spark of Feedback", 3277705907), |
|
([(4, 10)], "Spark of Brilliance", 3277705905), |
|
], |
|
[ # Solar |
|
([(1, 10)], "Ember Of Wonder", 1051276350), ([(1, -10)], "Ember Of Empyrean", 362132294), |
|
([(2, 10)], "Ember Of Searing", 1051276351), ([(2, -10)], "Ember Of Tempering", 362132290), |
|
([(3, 10)], "Ember Of Char", 362132291), ([(3, -10)], "Ember Of Benelovence", 362132292), |
|
([(4, 10)], "Ember Of Beams", 362132295), ([(5, 10)], "Ember Of Eruption", 1051276348), |
|
([(5, 10)], "Ember Of Combustion", 362132289), |
|
], |
|
[ # void |
|
([(2, -10)], "Echo Of Starvation", 2661180603), ([(4, -10)], "Echo Of Harvest", 2661180601), |
|
([(2, 10)], "Echo Of Obscurity", 2661180602), ([(5, 10)], "Echo Of Instability", 2661180600), |
|
([(3, -20)], "Echo Of Undermining", 2272984668), ([(3, 10)], "Echo Of Domineering", 2272984657), |
|
([(1, 10)], "Echo Of Leeching", 2272984670), ([(4, 10)], "Echo Of Expulsion", 2272984665), |
|
([(6, -10)], "Echo Of Persistence", 2272984671), ([(5, -10)], "Echo Of Provision", 2272984664), |
|
([(0, 10), (4, 10)], "Echo Of Dilation", 2272984656), |
|
], |
|
[ # stasis |
|
([(0, -10), (2, -10)], "Whisper Of Hunger", 2483898431), ([(3, -10)], "Whisper Of Fractures", 537774542), |
|
([(5, -10)], "Whisper Of Hedrons", 3469412970), ([(2, -10), (3, -10)], "Whisper Of Bonds", 3469412974), |
|
([(1, 10), (4, 10)], "Whisper Of Conduction", 2483898429), ([(1, 10)], "Whisper Of Shards", 3469412975), |
|
([(2, 10)], "Whisper Of Chains", 537774540), ([(5, 10)], "Whisper Of Durance", 3469412969), |
|
] |
|
] |
|
|
|
# Exotic intrinsic stats, per class (outer layer) and slot (inner layer) |
|
possibleBonusStats = [ |
|
# Titan |
|
[[[0, 1, 1], [0, 2, 0]], |
|
[[0, 1, 1], [0, 2, 0], [0, 2, 1]], |
|
[[2, 1, 0], [1, 1, 1], [0, 2, 1]], |
|
[[1, 1, 0], [1, 0, 1], [0, 2, 0]]], |
|
# Hunter |
|
[[[2, 0, 0], [1, 1, 0], [1, 0, 1]], |
|
[[1, 1, 1], [0, 1, 1], [1, 0, 1], [1, 1, 0], [2, 0, 0]], |
|
[[2, 0, 1], [2, 1, 0], [1, 1, 1], [1, 2, 0]], |
|
[[2, 1, 0], [2, 0, 0], [1, 1, 0]]], |
|
# Warlock |
|
[[[0, 1, 1], [0, 0, 2], [1, 0, 1]], |
|
[[0, 0, 2], [0, 2, 1], [0, 1, 1], [1, 0, 1]], |
|
[[2, 0, 1], [0, 2, 1], [0, 1, 2]], |
|
[[0, 1, 2], [1, 0, 1], [0, 1, 1]]] |
|
] |
|
# If we do not want to use exotic intrinsics, this will simply set the array to be empty and thus disables the feature. |
|
if not GENERATOR_USE_EXOTIC_INTRINSICS: |
|
possibleBonusStats = [[[], [], [], []], [[], [], [], []], [[], [], [], []]] |
|
for s, k in enumerate(GENERATOR_USE_EXOTIC_INTRINSICS_SLOTS): |
|
if not k: |
|
for clazz in range(0, 3): |
|
possibleBonusStats[clazz][s] = [] |
|
|
|
# Define the usable plugs for armor generation |
|
plugs = np.array([[1, 1, 10], [1, 1, 11], [1, 1, 12], [1, 1, 13], [1, 1, 14], [1, 1, 15], |
|
[1, 5, 5], [1, 5, 6], [1, 5, 7], [1, 5, 8], [1, 5, 9], [1, 5, 10], |
|
[1, 5, 11], [1, 6, 5], [1, 6, 6], [1, 6, 7], [1, 6, 8], [1, 6, 9], |
|
[1, 7, 5], [1, 7, 6], [1, 7, 7], [1, 7, 8], [1, 8, 5], [1, 8, 6], |
|
[1, 8, 7], [1, 9, 5], [1, 9, 6], [1, 10, 1], [1, 10, 5], [1, 11, 1], |
|
[1, 11, 5], [1, 12, 1], [1, 13, 1], [1, 14, 1], [1, 15, 1], [5, 1, 5], |
|
[5, 1, 6], [5, 1, 7], [5, 1, 8], [5, 1, 9], [5, 1, 10], [5, 1, 11], |
|
[5, 5, 1], [5, 5, 5], [5, 6, 1], [5, 7, 1], [5, 8, 1], [5, 9, 1], |
|
[5, 10, 1], [5, 11, 1], [6, 1, 5], [6, 1, 6], [6, 1, 7], [6, 1, 8], |
|
[6, 1, 9], [6, 5, 1], [6, 6, 1], [6, 7, 1], [6, 8, 1], [6, 9, 1], |
|
[7, 1, 5], [7, 1, 6], [7, 1, 7], [7, 1, 8], [7, 5, 1], [7, 6, 1], |
|
[7, 7, 1], [7, 8, 1], [8, 1, 5], [8, 1, 6], [8, 1, 7], [8, 5, 1], |
|
[8, 6, 1], [8, 7, 1], [9, 1, 5], [9, 1, 6], [9, 5, 1], [9, 6, 1], |
|
[10, 1, 1], [10, 1, 5], [10, 5, 1], [11, 1, 1], [11, 1, 5], [11, 5, 1], |
|
[12, 1, 1], [13, 1, 1], [14, 1, 1], [15, 1, 1]]) |
|
|
|
# Just some names required to print the results or to access specific fields of d2ap_armor.json entries. |
|
stat_names = ["mobility", "resilience", "recovery", "discipline", "intellect", "strength", "class ability"] |
|
slot_names = ["Helmet", "Gauntlet", "Chest", "Leg"] |
|
|
|
############################## |
|
# Validation Section |
|
# This section validates your configuration |
|
# DO NOT MODIFY ANYTHING IN THIS SECTION OR BELOW IT EXCEPT IF YOU KNOW WHAT YOU ARE DOING |
|
|
|
if not os.path.exists("d2ap_armor.json"): |
|
print("!!Warning!! d2ap_armor.json not found - setting USE_OWN_ARMOR to False") |
|
USE_OWN_ARMOR = False |
|
|
|
if USE_OWN_ARMOR: |
|
with open("d2ap_armor.json", "r") as f: |
|
armorData = json.load(f) |
|
else: |
|
armorData = [] |
|
|
|
if not USE_OWN_ARMOR and not GENERATOR_ENABLED: |
|
raise Exception("Configuration error: USE_OWN_ARMOR and GENERATOR_ENABLED must not be disabled at the same time.") |
|
if MIN_100_STATS < 0 or MIN_100_STATS > 5: |
|
raise Exception("Configuration error: MIN_100_STATS must be in the range 0 <= x <= 4, but is %d." % MIN_100_STATS) |
|
if AVAILABLE_MODS < 0 or AVAILABLE_MODS > 5: |
|
raise Exception("Configuration error: AVAILABLE_MODS must be in the range 0 <= x <= 5, but is %d." % AVAILABLE_MODS) |
|
if MODEL_TIMELIMIT_SECONDS <= 0: |
|
raise Exception("Configuration error: " |
|
"MODEL_TIMELIMIT_SECONDS must be larger than 0, but is %d." % MODEL_TIMELIMIT_SECONDS) |
|
if ARMOR_CLASS != -1 and (ARMOR_CLASS < 0 or ARMOR_CLASS > 2): |
|
raise Exception("Configuration error: " |
|
"ARMOR_CLASS must be 0 (Titan), 1 (Hunter) or 2 (Warlock), but is %d." % ARMOR_CLASS) |
|
if any([not isinstance(k, int) for k in IGNORED_ARMOR]): |
|
raise Exception("Configuration error: " |
|
"Every entry of IGNORED_ARMOR must be a number, but at least one isn't.") |
|
if any([any(not isinstance(m, int) for m in k) for k in REQUIRED_ARMOR_HASHES]): |
|
raise Exception("Configuration error: " |
|
"Every entry of REQUIRED_ARMOR_HASHES must be a number, but at least one isn't.") |
|
|
|
desired_stat_tier_sum = sum([k // 10 for k in DESIRED_STATS]) |
|
if desired_stat_tier_sum == 47: |
|
print("!!WARNING!!\t\t" |
|
"Note that the stat tier T47 can only be achieved in theory. " |
|
"Your configuration of DESIRED_STATS requires at least %d tiers." % desired_stat_tier_sum) |
|
elif desired_stat_tier_sum > 47: |
|
print("!!WARNING!!\t\t" |
|
"Stat tiers above T47 are impossible to reach. " |
|
"Your configuration of DESIRED_STATS requires at least %d tiers." % desired_stat_tier_sum) |
|
if sum(DESIRED_STATS) - sum(AVAILABLE_BONUS) - 10 * AVAILABLE_MODS > 335: |
|
print("!!WARNING!!\t\t" |
|
"Your configuration of DESIRED_STATS and AVAILABLE_BONUS is probably impossible to solve without fragments. " |
|
"AVAILABLE_BONUS does not give enough stat points to generate DESIRED_STATS." |
|
) |
|
|
|
if MINIMUM_TIERS == 47: |
|
print("!!WARNING!!\t\tNote that the stat tier T47 can only be achieved in theory.") |
|
elif MINIMUM_TIERS > 47: |
|
print("!!WARNING!!\t\tStat tiers above T47 are impossible to reach.") |
|
if MAXIMUM_STAT_VALUE < 100: |
|
print("!!WARNING!!\t\t" |
|
"It is not recommended to set MAXIMUM_STAT_VALUE below 100. Current value is %d." % MAXIMUM_STAT_VALUE) |
|
|
|
if WASTE_PENALTY_WEIGHT < 0: |
|
print("!!WARNING!!\t\t" |
|
"WASTE_PENALTY_WEIGHT should not be below 0, but is %d. " |
|
"You are telling the script to prefer wasted stats." % WASTE_PENALTY_WEIGHT) |
|
elif WASTE_PENALTY_WEIGHT == 0: |
|
print("!!WARNING!!\t\t" |
|
"WASTE_PENALTY_WEIGHT is 0. This is fine, but the tool will not weight in wasted stats in its result selection.") |
|
|
|
|
|
def filterArmor(a, slot): |
|
return ("slot" in a |
|
and a["slot"] == slot |
|
and (ARMOR_CLASS == -1 or a["clazz"] == ARMOR_CLASS) |
|
and int(a["itemInstanceId"]) not in IGNORED_ARMOR |
|
and (slot == 5 or len(REQUIRED_ARMOR_HASHES[slot - 1]) == 0 or int(a["hash"]) in REQUIRED_ARMOR_HASHES[slot - 1])) |
|
|
|
|
|
armor = [ |
|
[a for a in armorData if filterArmor(a, 1)], |
|
[a for a in armorData if filterArmor(a, 2)], |
|
[a for a in armorData if filterArmor(a, 3)], |
|
[a for a in armorData if filterArmor(a, 4)], |
|
] |
|
|
|
if any([len(i) == 0 and len(REQUIRED_ARMOR_HASHES[k]) > 0 for k, i in enumerate(armor)]): |
|
print("!!WARNING!!\t\t" |
|
"Your configuration of REQUIRED_ARMOR_HASHES resulted in an empty array of armor. " |
|
"This can potentially lead to invalid results.") |
|
|
|
######################################################################## |
|
######################################################################## |
|
# Code |
|
# DO NOT MODIFY ANYTHING IN THIS SECTION OR BELOW IT EXCEPT IF YOU KNOW WHAT YOU ARE DOING |
|
solver = pl.CPLEX_CMD() |
|
model = pl.LpProblem("Stats", pl.LpMaximize) |
|
|
|
# Step 1: Create variables for the stats themselves |
|
# This is an array of 6 stats (stat0 to stat5), which represent out stats in the following order: |
|
# 0: mobility 1: resilience 2: recovery |
|
# 3: discipline 4: intellect 5: strength |
|
v_total_stats = pl.LpVariable.dicts('stat', range(0, 6), lowBound=0, upBound=0, cat='Integer') |
|
|
|
# Every armor gets a masterwork, so we just add a fixed 10 to the armor stat |
|
# TODO: There is currently no setting for "assume masterwork" (which this is) |
|
# The purpose of this script is to maximize things for theorycraftig, |
|
# so I do not see the necessity right now. |
|
for n in range(0, 6): |
|
v_total_stats[n] += 10 # Masterworks :) |
|
|
|
v_character_class = pl.LpVariable.dicts('class', range(0, 3), lowBound=0, upBound=1, cat='Integer') |
|
model += pl.lpSum(v_character_class) == 1 |
|
# fix the selected class to 1 |
|
if ARMOR_CLASS > -1: |
|
model += v_character_class[ARMOR_CLASS] == 1 |
|
|
|
v_aspect_slots = 0 |
|
if GENERATOR_ENABLE_FRAGMENT_SELECTION: |
|
v_subcls = pl.LpVariable.dicts('subclass', range(0, 4), lowBound=0, upBound=1, cat='Integer') |
|
model += pl.lpSum(v_subcls) == 1 |
|
|
|
if GENERATOR_SELECTED_SUBCLASS > -1: |
|
# enforce the selected subcass |
|
model += v_subcls[GENERATOR_SELECTED_SUBCLASS] == 1 |
|
|
|
# We also select aspects to get the slots we need for the fragments. |
|
# this also means that we have pl.lpSum(v_aspect_slots) as the amount of free fragment slots |
|
aspectCount = sum([sum(len(y) for y in x) for x in subclassAspects]) |
|
v_aspects = pl.LpVariable.dicts('subclass_aspects', range(0, aspectCount), lowBound=0, upBound=1, cat='Integer') |
|
v_aspect_slots = pl.LpVariable("subclass_aspect_slots", lowBound=0, upBound=0, cat='Integer') |
|
model += pl.lpSum(v_aspects) <= 2 |
|
n = 0 |
|
for subclsId, subcls in enumerate(subclassAspects): |
|
for currentClass, classEntry in enumerate(subcls): |
|
for aspect in classEntry: |
|
if ARMOR_CLASS > -1 and ARMOR_CLASS != currentClass: |
|
model += v_aspects[n] == 0 |
|
else: |
|
model += v_aspects[n] <= v_subcls[subclsId] |
|
model += v_aspects[n] <= v_character_class[currentClass] |
|
v_aspect_slots += v_aspects[n] * aspect[0] |
|
n += 1 |
|
|
|
# and now we select fragments to fulfill our stat requirements |
|
fragmentCount = sum([len(k) for k in subclassFragments]) |
|
v_fragments = pl.LpVariable.dicts('subclass_fragments', range(0, aspectCount), lowBound=0, upBound=1, cat='Integer') |
|
model += pl.lpSum(v_fragments) <= v_aspect_slots |
|
n = 0 |
|
for subclsId, subcls in enumerate(subclassFragments): |
|
for fragment in subcls: |
|
# Disable this fragment if we chose a different subclass |
|
model += v_fragments[n] <= v_subcls[subclsId] |
|
# Enforce this fragment if it has been set in REQUIRED_FRAGMENT_HASHES |
|
if fragment[2] in REQUIRED_FRAGMENT_HASHES: |
|
model += v_fragments[n] == 1 |
|
# Disable this fragment if it has been set in IGNORED_FRAGMENT_HASHES |
|
if fragment[2] in IGNORED_FRAGMENT_HASHES: |
|
model += v_fragments[n] == 0 |
|
|
|
for stat_boost in fragment[0]: |
|
stat, boost = stat_boost |
|
if stat == 6: |
|
# Class based stats |
|
v_lookup = pl.LpVariable.dicts(fragment[1], range(0, 3), lowBound=0, upBound=1, cat='Integer') |
|
model += pl.lpSum(v_lookup) <= 1 |
|
model += pl.lpSum(v_lookup) >= v_fragments[n] |
|
v_total_stats[0] += boost * v_lookup[1] |
|
v_total_stats[1] += boost * v_lookup[2] |
|
v_total_stats[2] += boost * v_lookup[0] |
|
else: |
|
# all other stats |
|
v_total_stats[stat] += boost * v_fragments[n] |
|
n += 1 |
|
|
|
if GENERATOR_ENABLE_COMBATSTYLE_SELECTION: |
|
v_combatstyle = pl.LpVariable.dicts('combatstyle', range(0, len(combatStyleMods)), lowBound=0, upBound=1, cat='Integer') |
|
v_combatstyle_sum = pl.lpSum(v_combatstyle) |
|
model += pl.lpSum(v_combatstyle) >= 0 |
|
model += pl.lpSum(v_combatstyle) <= 5 |
|
|
|
for n, modData in enumerate(combatStyleMods): |
|
v_total_stats[modData[0][0]] += modData[0][1] * v_combatstyle[n] |
|
|
|
# If we added static boosts in the configuration, we add them right here. |
|
# Static boosts are just added to the baseline of the stat, before any armor calculation has been done. |
|
if np.any(AVAILABLE_BONUS): |
|
for n in range(0, 6): |
|
v_total_stats[n] += AVAILABLE_BONUS[n] |
|
|
|
if AVAILABLE_MODS > 0: |
|
# Register the stat mod variables. We allow for major and minor stat mods. |
|
# This is important in the case that we want to optimize for reduced wasted stats. |
|
v_mods_major = pl.LpVariable.dicts('modMajor', range(0, 6), cat='Integer', lowBound=0, upBound=5) |
|
v_mods_minor = pl.LpVariable.dicts('modMinor', range(0, 6), cat='Integer', lowBound=0, upBound=5) |
|
model += pl.lpSum(v_mods_major) + pl.lpSum(v_mods_minor) <= AVAILABLE_MODS |
|
if AVAILABLE_MODS > 0: |
|
for mod in range(0, 6): |
|
v_total_stats[mod] += 10 * v_mods_major[mod] |
|
v_total_stats[mod] += 5 * v_mods_minor[mod] |
|
|
|
# Apply items |
|
# The basic idea is that we want it to use the items in our inventory. |
|
# If it does not find a possible solution with our items, then it should propose which armor to grind. |
|
# We do this by giving every item entry a "point score". |
|
# The armor we have in our inventory gets a high score, and "generated" items get a low score. |
|
# Later, we want to maximize the score. |
|
# Due to the limits we applied, the MIP is then forced to fill impossible slots with fictional items. |
|
v_armor_scores = pl.LpVariable.dicts('armor_score', range(0, 4), lowBound=0, upBound=0, cat='Integer') |
|
# TODO: here is a small bug that happens when you use your own armor. If upBound is not 0, then it will possible generate builds without exotics. |
|
# Not harmful at all, but noteworthy. |
|
v_armor_exotic = pl.LpVariable.dicts('armor_exotic', range(0, 4), lowBound=0, upBound=1, # if not USE_OWN_ARMOR else 0, |
|
cat='Integer') |
|
armor_reference = [] |
|
custom_armor = [] |
|
for armorId in range(0, 4): |
|
armorList = armor[armorId] |
|
# Create a runtime variable with len(armorList) + 1; We use this to select our armor later. |
|
# The +1 entry is the generated fallback armor piece |
|
# The MIP will have to select ONE entry from here. We use this variable to grab stats |
|
v_armort = pl.LpVariable.dicts('armor_%d_selection' % armorId, range(0, len(armorList) + 1), lowBound=0, upBound=1, |
|
cat='Integer') |
|
armor_reference.append(v_armort) |
|
model += pl.lpSum(v_armort) == 1 # only select exactly one |
|
|
|
if not GENERATOR_ENABLED: |
|
model += v_armort[0] == 0 |
|
|
|
if GENERATOR_ENABLED and (not REQUIRED_ARMOR_ENFORCED or len(REQUIRED_ARMOR_HASHES[armorId]) == 0): |
|
# register the generated fallback armor piece |
|
v_item_stats = pl.LpVariable.dicts('armor_%d_stat' % armorId, range(0, 6), lowBound=0, upBound=0, cat='Integer') |
|
v_item_plugs_g1 = pl.LpVariable.dicts('armor_%d_plug1' % armorId, range(0, len(plugs)), lowBound=0, upBound=2, |
|
cat='Integer') |
|
v_item_plugs_g2 = pl.LpVariable.dicts('armor_%d_plug2' % armorId, range(0, len(plugs)), lowBound=0, upBound=2, |
|
cat='Integer') |
|
# this forces 0 plugs if the fallback armor is not selected |
|
model += pl.lpSum(v_item_plugs_g1) == v_armort[0] * 2 |
|
# this forces 0 plugs if the fallback armor is not selected |
|
model += pl.lpSum(v_item_plugs_g2) == v_armort[0] * 2 |
|
|
|
# Apply exotic intrinsic stats |
|
v_exo = pl.LpVariable("armor_gen_ex_%d" % armorId, lowBound=0, upBound=1, cat='Integer') |
|
model += v_exo <= v_armort[0] |
|
model += v_exo <= v_armor_exotic[armorId] |
|
|
|
intrinsicCount = sum([sum(len(y) for y in x) for x in possibleBonusStats]) |
|
v_intrinsics = pl.LpVariable.dicts("armor_gen_intrinsics_%d" % armorId, |
|
range(0, intrinsicCount), lowBound=0, |
|
upBound=1, cat='Integer') |
|
# only allow intrinsic stats if it is an exotic. we do this by limiting the whole intrinsic selector by 0 |
|
model += pl.lpSum(v_intrinsics) <= v_exo |
|
|
|
n = 0 |
|
for clazzId, clazzEntries in enumerate(possibleBonusStats): |
|
for entry in clazzEntries[armorId]: |
|
# Limit the intrinsic stat(s) to the class it originates from |
|
model += v_intrinsics[n] <= v_character_class[clazzId] |
|
for r in [0, 1, 2]: |
|
if entry[r] > 0: |
|
v_item_stats[r] += entry[r] * v_intrinsics[n] |
|
n += 1 |
|
|
|
# Add the plugs, which basically generates this armor set |
|
for n in range(0, 3): |
|
for k, v in enumerate(plugs): |
|
v_item_stats[n] += v_item_plugs_g1[k] * plugs[k][n] |
|
v_item_stats[3 + n] += v_item_plugs_g2[k] * plugs[k][n] |
|
|
|
v_total_stats[n] += v_item_stats[n] |
|
v_total_stats[3 + n] += v_item_stats[3 + n] |
|
custom_armor.append((v_item_stats, v_intrinsics)) |
|
|
|
# now register our armor items |
|
for kArmor, armorEntry in enumerate(armorList): |
|
# Disable armor if it does not fit to the class the MIP selected. |
|
model += v_armort[kArmor + 1] <= v_character_class[armorEntry["clazz"]] |
|
# mark this armor as an exotic. This means that the result will only have up to one exotic |
|
if armorEntry["isExotic"]: |
|
v_armor_exotic[armorId] += v_armort[kArmor + 1] |
|
# our armor is worth one point, while the generated one is not worth anything |
|
v_armor_scores[armorId] += v_armort[kArmor + 1] * 1 |
|
# add another, higher score if we set entries in the REQUIRED_ARMOR_HASHES list. |
|
# This means we really want these items in the result |
|
if len(REQUIRED_ARMOR_HASHES[armorId]) > 0: |
|
v_armor_scores[armorId] += v_armort[kArmor + 1] * 2 |
|
for kStat, stat in enumerate(stat_names[0:6]): |
|
v_total_stats[kStat] += v_armort[kArmor + 1] * armorEntry[stat] |
|
|
|
# Only allow one exotic |
|
model += pl.lpSum(v_armor_exotic) <= 1 |
|
if REQUIRE_EXOTIC: |
|
model += pl.lpSum(v_armor_exotic) == 1 |
|
|
|
# Apply a limit on each stat. |
|
# The lower bound is the value in DESIRED_STATS, which means it HAS to have this value. |
|
# The upper bound is just a fixed value to stop it from overflowing. |
|
for stat in v_total_stats: |
|
if DESIRED_STATS_EXACT: |
|
model += v_total_stats[stat] == DESIRED_STATS[stat] |
|
else: |
|
model += v_total_stats[stat] >= DESIRED_STATS[stat] |
|
if MAXIMUM_STAT_VALUE > -1: |
|
model += v_total_stats[stat] <= MAXIMUM_STAT_VALUE |
|
#################################### |
|
# Calculation variables |
|
tiers = pl.LpVariable.dicts('tier', range(0, 6), lowBound=0, cat='Integer') |
|
for stat in range(0, 6): |
|
model += tiers[stat] >= 0 |
|
if MAXIMUM_STAT_VALUE > 0: |
|
model += tiers[stat] <= MAXIMUM_STAT_VALUE // 10 |
|
|
|
model += tiers[stat] >= (v_total_stats[stat] / 10) - 0.9 |
|
model += tiers[stat] <= (v_total_stats[stat] / 10) |
|
|
|
v_tier_sum = pl.lpSum(tiers) |
|
model += v_tier_sum >= 0 |
|
model += v_tier_sum >= MINIMUM_TIERS |
|
|
|
# Let's check how many 100 stats you have |
|
if MIN_100_STATS > 0: |
|
v_x100_stats = pl.LpVariable.dicts('x100stat', range(0, 6), lowBound=0, cat='Integer') |
|
v_x100_stat_count = pl.lpSum(v_x100_stats) |
|
for n in range(0, 6): |
|
model += v_x100_stats[n] >= (v_total_stats[n] / 100) - 0.99 |
|
model += v_x100_stats[n] <= (v_total_stats[n] / 100) |
|
|
|
model += v_x100_stat_count >= MIN_100_STATS |
|
|
|
v_stat_sum = pl.lpSum(v_total_stats) |
|
v_armor_scores_sum = pl.lpSum(v_armor_scores) |
|
|
|
v_waste = pl.LpVariable('waste', cat="Integer") |
|
model += v_waste == pl.lpSum([pl.lpSum(v_total_stats[stat] - tiers[stat] * 10) for stat in range(0, 6)]) |
|
if MAXIMUM_STAT_WASTE == 0: |
|
model += pl.lpSum([pl.lpSum(v_total_stats[stat] - tiers[stat] * 10) for stat in range(0, 6)]) == 0 |
|
elif MAXIMUM_STAT_WASTE > -1: |
|
model += v_waste <= MAXIMUM_STAT_WASTE |
|
|
|
######### |
|
# grading Weights |
|
# Tell the model what we want to optimize. |
|
# In summa: |
|
# - Use as many items in our vault as we can. Only resort to armor generation as a last resort. |
|
# - Then we want as many tiers as possible. Basically, any T34 is better than T33 |
|
# - Inside the same tier, we now optimize for stats. |
|
# - We also give wasted stats a penalty, as we do not want them at all. |
|
model += ( |
|
# we want to use as many of our own armor pieces as possible. |
|
# Even one generated one is a HUUUUGE penalty! |
|
+ 10000 * v_armor_scores_sum |
|
+ 10 * v_tier_sum # We want as many tiers as possible |
|
+ (0 if MIN_100_STATS == 0 else 10 * v_x100_stat_count) # We want as many tiers as possible |
|
+ 1 * v_stat_sum # Then we want as many stat points as possible |
|
- (0 if not GENERATOR_ENABLE_COMBATSTYLE_SELECTION or STAT_MOD_PENALTY_WEIGHT == 0 else STAT_MOD_PENALTY_WEIGHT * v_combatstyle_sum) |
|
# we want to be able to give weights to each stat, basically telling the script to prefer |
|
# or to dislike a certain stat |
|
+ (0 if STAT_WEIGHT_MOBILITY == 0 else STAT_WEIGHT_MOBILITY * v_total_stats[0]) |
|
+ (0 if STAT_WEIGHT_RESILIENCE == 0 else STAT_WEIGHT_RESILIENCE * v_total_stats[1]) |
|
+ (0 if STAT_WEIGHT_RECOVERY == 0 else STAT_WEIGHT_RECOVERY * v_total_stats[2]) |
|
+ (0 if STAT_WEIGHT_DISCIPLINE == 0 else STAT_WEIGHT_DISCIPLINE * v_total_stats[3]) |
|
+ (0 if STAT_WEIGHT_INTELLECT == 0 else STAT_WEIGHT_INTELLECT * v_total_stats[4]) |
|
+ (0 if STAT_WEIGHT_STRENGTH == 0 else STAT_WEIGHT_STRENGTH * v_total_stats[5]) |
|
# and of course we do not like wasted points, so we simply remove them from the stat sum. |
|
- (0 if WASTE_PENALTY_WEIGHT == 0 else WASTE_PENALTY_WEIGHT * v_waste) |
|
# Now let's add weights for aspects and fragments. |
|
# I want to get the best stats with the least amount of fragments |
|
# That means that I will add a penalty for each used fragment |
|
- (0 if (GENERATOR_ENABLE_FRAGMENT_SELECTION is False or FRAGMENT_SLOT_PENALTY_WEIGHT == 0) else |
|
FRAGMENT_SLOT_PENALTY_WEIGHT * v_aspect_slots) |
|
- (0 if GENERATOR_ENABLE_FRAGMENT_SELECTION is False or FRAGMENT_PENALTY_WEIGHT == 0 else |
|
FRAGMENT_PENALTY_WEIGHT * pl.lpSum(v_fragments)) |
|
|
|
) |
|
|
|
result = model.solve(pl.PULP_CBC_CMD( |
|
timeLimit=MODEL_TIMELIMIT_SECONDS, |
|
msg=MODEL_PRINT_DEBUG_MESSAGES, |
|
keepFiles=False |
|
)) |
|
|
|
print("~~~ Input ~~~") |
|
print("Desired stats:", DESIRED_STATS) |
|
print("Available bonus:", AVAILABLE_BONUS) |
|
print("Available Mods", AVAILABLE_MODS) |
|
if model.status == 1: |
|
print('Solution is optimal: %s' % pl.LpStatus[model.status]) |
|
else: |
|
print('!! Failed to find solution: %s !!' % pl.LpStatus[model.status]) |
|
print('This means that your configuration creates constraints that are not possible.') |
|
print('Adapt the settings, and keep in mind that some distributions simply are impossible.') |
|
|
|
print("v_armor_scores_sum", pl.value(v_armor_scores_sum)) |
|
print("v_tier_sum", pl.value(v_tier_sum)) |
|
print("v_character_class", [pl.value(v_character_class[k]) for k in v_character_class]) |
|
print("v_total_stats", [pl.value(v_total_stats[k]) for k in v_total_stats]) |
|
print("tiers", [pl.value(tiers[k]) for k in tiers]) |
|
if AVAILABLE_MODS > 0: |
|
print("v_mods_major", [pl.value(v_mods_major[k]) for k in v_mods_major]) |
|
print("v_mods_minor", [pl.value(v_mods_minor[k]) for k in v_mods_minor]) |
|
if MIN_100_STATS > 0: |
|
print("x100", pl.value(pl.lpSum(v_x100_stat_count))) |
|
print("v_waste", pl.value(v_waste)) |
|
print("v_stat_sum", pl.value(v_stat_sum)) |
|
print("exotic", [pl.value(v_armor_exotic[k]) for k in range(0, 4)]) |
|
raise Exception('Failed to find solution: %s' % pl.LpStatus[model.status]) |
|
print() |
|
|
|
chosen_clazz = np.where(np.array([pl.value(v_character_class[c]) for c in v_character_class]) == 1)[0][0] |
|
|
|
print("~~~ OUTPUT ~~~") |
|
print("Class: ", ["Titan", "Hunter", "Warlock"][chosen_clazz]) |
|
if GENERATOR_ENABLE_FRAGMENT_SELECTION: |
|
subcls = np.where(np.array([pl.value(v_subcls[k]) for k in v_subcls]) == 1)[0][0] |
|
print("Subclass: ", ["Arc", "Solar", "Void", "Stasis"][subcls]) |
|
aspects_flat = [x for g in subclassAspects for f in g for x in f] |
|
aspects_indexes = np.where(np.array([pl.value(v_aspects[k]) for k in v_aspects]) == 1)[0] |
|
if len(aspects_indexes) > 0: |
|
print("Selected Aspects: ", end="") |
|
for k in aspects_indexes: |
|
print(aspects_flat[k][1], end=", ") |
|
print() |
|
|
|
print("%-18s %-5d" % ("Total Stat Points:", int(pl.value(v_stat_sum)))) |
|
print("%-18s %-4d" % ("Total Tiers:", int(pl.value(pl.lpSum(tiers))))) |
|
print("") |
|
|
|
print("Use the following armor pieces and mods:") |
|
print("%-11s%-13s%-23s%-26s%-35s" % ("Slot", "Hash", "itemInstanceId", "Stats", "Name")) |
|
|
|
formatStats = lambda st: "[%s]" % ",".join(["%3d" % s for s in st]) |
|
printPlaceholder = lambda: print("%-11s%-13s%-23s%-26s%-35s" % ( |
|
"-----------", "-------------", "-----------------------", |
|
"--------------------------", "-----------------------------------")) |
|
|
|
for k, armorRef in enumerate(armor_reference): |
|
selected = np.where(np.array([pl.value(armorRef[p]) == 1 for p in armorRef]) == 1)[0] |
|
if len(selected) == 0: |
|
print("~~ NO ARMOR COULD BE FOUND FOR THIS SLOT ~~ CONFIGURATION NOT POSSIBLE ~~") |
|
continue |
|
|
|
selected = selected[0] |
|
if selected > 0: |
|
arm = armor[k][selected - 1] |
|
print("%-11s%-13s%-23s%-26s%-35s" % (slot_names[k], arm["hash"], arm["itemInstanceId"], |
|
formatStats([ |
|
arm["mobility"], arm["resilience"], arm["recovery"], |
|
arm["discipline"], arm["intellect"], arm["strength"], |
|
]), arm["name"] |
|
)) |
|
else: |
|
# print a message in case that we wanted to use our own armor, but couldn't because it is impossible. |
|
# this only happens if REQUIRED_ARMOR_ENFORCED is False |
|
if len(REQUIRED_ARMOR_HASHES[k]) > 0: |
|
print( |
|
"%-11sThe armor set in REQUIRED_ARMOR_HASHES could not satisfy the desired stats. Farm the following:" |
|
% (slot_names[k])) |
|
|
|
print("%-11s%-13s%-23s%-26s" % (slot_names[k], "theory", "Grind this %s" % slot_names[k], |
|
formatStats( |
|
[int(pl.value(custom_armor[k][0][p])) for p in custom_armor[k][0]])), |
|
end="") |
|
if pl.value(v_armor_exotic[k]) == 1: |
|
intrinsicIndex = np.array([pl.value(custom_armor[k][1][r]) for r in custom_armor[k][1]]) |
|
if sum(intrinsicIndex) > 0: |
|
plugIndex = np.where(intrinsicIndex == 1)[0][0] |
|
bonus_flat = [y for g in possibleBonusStats for km, f in enumerate(g) for y in f if km == k] |
|
stats = bonus_flat[plugIndex] |
|
print("exotic with intrinsic stats %s" % str(stats), end="") |
|
print("") |
|
|
|
print("%-11s%-13s%-23s%-26s%-35s" % ("Masterwork", "--", "--", formatStats([10, 10, 10, 10, 10, 10]), "")) |
|
|
|
printPlaceholder() |
|
|
|
# print mods |
|
if AVAILABLE_MODS > 0: |
|
print("%-11s%-13s%-23s%-26s%-35s" % ("Stat Mods", "--", "--", |
|
formatStats( |
|
[pl.value(v_mods_major[k]) * 10 + pl.value(v_mods_minor[k]) * 5 |
|
for k in range(0, 6)]), "")) |
|
# print mods |
|
if sum(AVAILABLE_BONUS) > 0: |
|
print("%-11s%-13s%-23s%-26s%-35s" % ("Fixed Bonus", "--", "--", formatStats(AVAILABLE_BONUS), "")) |
|
|
|
if GENERATOR_ENABLE_COMBATSTYLE_SELECTION: |
|
csMods = [m for k, m in enumerate(combatStyleMods) if pl.value(v_combatstyle[k]) == 1] |
|
for mod in csMods: |
|
stats = [0, 0, 0, 0, 0, 0] |
|
stats[mod[0][0]] += mod[0][1] |
|
print("%-11s%-13s%-23s%-26s%-35s" % ("CS Mod", "--", "--", formatStats(stats), mod[1])) |
|
|
|
if GENERATOR_ENABLE_FRAGMENT_SELECTION: |
|
|
|
fragments_flat = [f for g in subclassFragments for f in g] |
|
fragment_indexes = np.where(np.array([pl.value(v_fragments[k]) for k in v_fragments]) == 1)[0] |
|
if len(fragment_indexes) > 0: |
|
for k in fragment_indexes: |
|
strp = fragments_flat[k][1] |
|
stats = [0, 0, 0, 0, 0, 0] |
|
for s in fragments_flat[k][0]: |
|
if s[0] == 6: |
|
s[0] = [1, 0, 2][chosen_clazz] |
|
stats[s[0]] += s[1] |
|
|
|
print("%-11s%-13s%-23s%-26s%-35s" % ("Fragment", "--", "--", formatStats(stats), strp)) |
|
|
|
printPlaceholder() |
|
# print stats |
|
|
|
print("%-11s%-13s%-23s%-26s%-35s" % ("Summary", "--", "--", |
|
formatStats([pl.value(v_total_stats[stat]) for stat in range(0, 6)]), "")) |
|
|
|
|
|
# Generate DIM Link |
|
def getModIds(): |
|
statModIds = [ |
|
# minor, major |
|
[204137529, 3961599962], # mob |
|
[3682186345, 2850583378], # res |
|
[555005975, 2645858828], # rec |
|
[2623485440, 4048838440], # dis |
|
[1227870362, 3355995799], # int |
|
[3699676109, 3253038666], # str |
|
] |
|
# Add stat mods |
|
usedModIds = [ |
|
[statModIds[int(k)][1] for c in range(0, int(pl.value(v_mods_major[k])))] |
|
+ [statModIds[k][0] for c in range(0, int(pl.value(v_mods_minor[k])))] |
|
for k in range(0, 6)] |
|
# Flatten the list |
|
usedModIds = [int(j) for sub in usedModIds for j in sub] |
|
|
|
# now add combat mods |
|
if GENERATOR_ENABLE_COMBATSTYLE_SELECTION: |
|
usedModIds += [m[2] for k, m in enumerate(combatStyleMods) if pl.value(v_combatstyle[k]) == 1] |
|
|
|
return usedModIds |
|
|
|
|
|
# Generate DIM link if all 4 items are my own |
|
if USE_OWN_ARMOR and pl.value(v_armor_scores_sum) == 4: |
|
import urllib.parse |
|
|
|
print() |
|
loadout = { |
|
"id": "mijago", |
|
"name": "Mijago Armor Calculator Loadout", |
|
"classType": int(chosen_clazz), |
|
"unequipped": [], |
|
"clearSpace": False, |
|
"parameters": { |
|
"statConstraints": [], |
|
"assumeArmorMasterwork": 3, # AssumeArmorMasterwork.All |
|
"lockArmorEnergyType": 1, # LockArmorEnergyType.None |
|
"mods": getModIds() |
|
}, |
|
"equipped": [] |
|
} |
|
|
|
# Equip armor |
|
for k, armorRef in enumerate(armor_reference): |
|
selected = np.where(np.array([pl.value(armorRef[p]) == 1 for p in armorRef]) == 1)[0][0] |
|
arm = armor[k][selected - 1] |
|
loadout["equipped"].append({ |
|
"id": arm["itemInstanceId"], |
|
"hash": arm["hash"], |
|
}) |
|
# add any class item |
|
clazzItems = [a for a in armorData if filterArmor(a, 5) and a["clazz"] == chosen_clazz] |
|
if len(clazzItems) > 0: |
|
clazzItems.sort(key=lambda d: d["energyLevel"], reverse=True) |
|
loadout["equipped"].append({ |
|
"id": clazzItems[0]["itemInstanceId"], |
|
"hash": clazzItems[0]["hash"], |
|
}) |
|
|
|
# equip fragments |
|
if GENERATOR_ENABLE_FRAGMENT_SELECTION: |
|
subcls = np.where(np.array([pl.value(v_subcls[k]) for k in v_subcls]) == 1)[0][0] |
|
|
|
subclassItem = { |
|
"id": "subclass", |
|
"hash": subclassHashes[chosen_clazz][subcls] |
|
} |
|
|
|
fragments_flat = [f for g in subclassFragments for f in g] |
|
fragment_indexes = np.where(np.array([pl.value(v_fragments[k]) for k in v_fragments]) == 1)[0] |
|
|
|
socketOverrides = {} |
|
if len(fragment_indexes) > 0: |
|
for k, idx in enumerate(fragment_indexes): |
|
socketOverrides[k + 7] = fragments_flat[idx][2] |
|
aspects_flat = [x for g in subclassAspects for f in g for x in f] |
|
aspects_indexes = np.where(np.array([pl.value(v_aspects[k]) for k in v_aspects]) == 1)[0] |
|
if len(aspects_indexes) > 0: |
|
for k, idx in enumerate(aspects_indexes): |
|
socketOverrides[k + 5] = aspects_flat[idx][2] |
|
subclassItem["socketOverrides"] = socketOverrides |
|
loadout["equipped"].append(subclassItem) |
|
|
|
if pl.value(pl.lpSum(v_armor_exotic)) == 1: |
|
exotic_index = int(np.where(np.array([pl.value(v_armor_exotic[k]) for k in v_armor_exotic]) == 1)[0]) |
|
armorRef = armor_reference[exotic_index] |
|
selected = np.where(np.array([pl.value(armorRef[p]) == 1 for p in armorRef]) == 1)[0][0] |
|
arm = armor[exotic_index][selected - 1] |
|
loadout["parameters"]["exoticArmorHash"] = arm["hash"] |
|
|
|
url = "https://app.destinyitemmanager.com/loadouts?loadout=" + urllib.parse.quote(json.dumps(loadout).encode("utf-8")) |
|
print(url) |