mirror of
https://github.com/Kvan7/Exiled-Exchange-2.git
synced 2026-09-20 19:55:52 +00:00
* Price checking does not work when using a gamepad(Ctrl+D) #452 Uses parts from #454 to fix the issue. Co-authored-by: lawrsp <7957003+lawrsp@users.noreply.github.com> * Fix tests :( * Fix magic rarity item name parse in "cmn-Hant" language (#460) * Fix magic rarity item name parse in "cmn-Hant" language * Add by translated when ref is null --------- Co-authored-by: kvan7 <kvan.valve@gmail.com> * chore: yarn to npm and add missing step (#461) * fix: update game config path for linux to poe2 * Merge pull request #463 from nightah/fix-poe2-linuxconfig fix: update game config path for linux to poe2 * fix tests * fix: MacOS crash on startup (#428) * fix: MacOS crash on startup * update for windows/linux * move main app startup into function Mac calls that in async, other platforms proceed in sync. * [PoE2] - Relics broken again #444 * test prettier and add npm script * add format/lint support to main * Fix defineProps macro * Run speed v0.8.0 - Russian. #447 * fix negative gold * Merge branch 'Kvan7:master' into master * fix: app-ready fixing before we're ready * Merge branch 'dev' into pr/larssn/428 * should work? * Merge commit 'ab1c8bfa3a31b06da9cf18db0273f6a92e407bc5' into pr/larssn/428 * fix being lazy on the merge * fix: add executable bit to compilation script (#465) * Item Images (#472) * image data stuff * ignore lookup file * update testing * sort items * change sort ot be by refname * more code * working pulling * Fixes #456 Create script to request item's images from trade site #456 * Fixes #457 Update ImageFix to use new saved images before poe1 ones #457 * add images to items * Add Spear as item category * Add Flail as category * Add "goodness" from upstream * Fix #474 Tier # missing from some defense stats #474 * minor oops * remove error for waystones * Add a bunch of images to the docs * Extra widgets docs * Update chat commands links docs * add stash search docs * Item info docs page * more docs * Update bug-report.yml * Version bump * extra version bump * Merge commit '77899fd6a7e08ab25bb677b70ba04757cc472f63' into dev * Upgraded rune selector #345 (#482) * Add added-rune mark to effected stats * Add runes to data parser * run data * more change * add popout to select rune * Other runes work now * different ui for opening rune menu * banner for debug cause i want it * Add above selector * Add auto close * Add config option to open above * update translation docs * More useful filter enables * Add stat text to rune selector * fix overflow * fix le garbage * Fix unique corrupted error * hide runes in 2 spots * fix ele damage runes * Fix bug no filters * minor change * fix flat damage tiers * fix source on ele damage * revert #103 * data update for timelost * fix warnings for timelost jewels * add tags to items data * Fix "grant skill" unidentified mod * Update style for rune, remove logs * better shortening for timelost text * Generalize the rune selector * Fix attack speed rune * fix runes including non martial weapons * sort trade ids, helps git struggle less * final data update before patch time * minor * update data for 0.2.0 * fix runes kinda * version bump
446 lines
14 KiB
Python
446 lines
14 KiB
Python
import logging
|
|
import os
|
|
import re
|
|
import uuid
|
|
from collections import defaultdict
|
|
from pprint import pprint
|
|
|
|
import numpy as np
|
|
import pandas as pd
|
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
|
|
def modTierBuilder(mod_data):
|
|
# First flatten out for each stat on the mod
|
|
flat_mods = []
|
|
logger.info("Starting to build mods")
|
|
logger.info(f"Incoming data: {len(mod_data)}")
|
|
for mod in mod_data:
|
|
mod_index = mod.get("_index")
|
|
if mod_index is None:
|
|
continue
|
|
mod_unique_id = mod.get("Id")
|
|
mod_type = mod.get("ModType")
|
|
mod_level = mod.get("Level")
|
|
mod_domain = mod.get("Domain")
|
|
mod_name = mod.get("Name")
|
|
mod_jewel_radius = mod.get("RadiusJewelType")
|
|
mod_stats = []
|
|
mod_stat_ids = []
|
|
for stat_index in range(1, 7):
|
|
stat = mod.get(f"Stat{stat_index}")
|
|
if stat is None:
|
|
continue
|
|
stat_value = mod.get(f"Stat{stat_index}Value")
|
|
if stat_value is None or not isinstance(stat_value, list):
|
|
continue
|
|
mod_stat_ids.append(stat)
|
|
mod_stats.append(stat_value)
|
|
|
|
mod_mod = {
|
|
"mod_index": mod_index,
|
|
"mod_unique_id": mod_unique_id,
|
|
"mod_type": mod_type,
|
|
"mod_level": mod_level,
|
|
"mod_domain": mod_domain,
|
|
"mod_name": mod_name,
|
|
"mod_jewel_radius": mod_jewel_radius,
|
|
"mod_stat_values": mod_stats,
|
|
"mod_stat_ids": mod_stat_ids,
|
|
}
|
|
flat_mods.append(mod_mod)
|
|
|
|
logger.info(f"Created {len(flat_mods)} mods")
|
|
# step 2
|
|
by_id = {}
|
|
for mod in flat_mods:
|
|
mod_id_with_trailing_num = mod.get("mod_unique_id")
|
|
mod_id = re.sub(r"\d+$", "", mod_id_with_trailing_num.strip())
|
|
if mod_id not in by_id:
|
|
by_id[mod_id] = []
|
|
by_id[mod_id].append(mod)
|
|
logger.info(f"Created {len(by_id)} unique mods")
|
|
|
|
# Filter duplicate stat1 ids, but shortest mod_id (keep shortest)
|
|
shortest_mod_id_by_stat1 = {}
|
|
for mod_id, mod_list in by_id.items():
|
|
stat_list = mod_list[0].get("mod_stat_ids")
|
|
if len(stat_list) < 1:
|
|
shortest_mod_id_by_stat1[uuid.uuid4()] = mod_id
|
|
continue
|
|
stat1_id = stat_list[0]
|
|
if stat1_id in shortest_mod_id_by_stat1:
|
|
if len(mod_id) < len(shortest_mod_id_by_stat1[stat1_id]):
|
|
shortest_mod_id_by_stat1[stat1_id] = mod_id
|
|
else:
|
|
shortest_mod_id_by_stat1[stat1_id] = mod_id
|
|
|
|
# Filter by_id to only include shortest mod_id keys
|
|
filtered_by_id = {}
|
|
for stat1_id, mod_id in shortest_mod_id_by_stat1.items():
|
|
filtered_by_id[mod_id] = by_id[mod_id]
|
|
|
|
logger.info(f"Created {len(filtered_by_id)} single stat mods")
|
|
|
|
return by_id
|
|
|
|
|
|
def get_tier_number_id(id):
|
|
if "shield" in id:
|
|
return "shield"
|
|
elif "armour" in id:
|
|
if "armour" == id:
|
|
return "armour"
|
|
else:
|
|
return "body_armour"
|
|
else:
|
|
return id
|
|
|
|
|
|
def get_gold_tags(mods_in, gold_mod_prices_in, tags_in):
|
|
mods = pd.DataFrame(mods_in)
|
|
gold_mod_prices = pd.DataFrame(gold_mod_prices_in)
|
|
tags = pd.DataFrame(tags_in)
|
|
|
|
replace = {
|
|
38: 7,
|
|
39: 7,
|
|
40: 7,
|
|
41: 7,
|
|
42: 7,
|
|
43: 7,
|
|
44: 7,
|
|
226: 1,
|
|
227: 1,
|
|
228: 1,
|
|
229: 1,
|
|
230: 1,
|
|
}
|
|
|
|
def process_tags(tags_list, weights_list):
|
|
# Replace tag IDs
|
|
new_tags = [replace[t] if t in replace else t for t in tags_list]
|
|
|
|
# Remove sequential duplicate 7s along with corresponding SpawnWeights
|
|
filtered_tags, filtered_weights = [], []
|
|
prev_tag = None
|
|
for tag, weight in zip(new_tags, weights_list):
|
|
if (tag == 7 and prev_tag == 7) or (tag == 1 and prev_tag == 1):
|
|
continue # Skip duplicate sequential 7
|
|
filtered_tags.append(tag)
|
|
filtered_weights.append(weight)
|
|
prev_tag = tag
|
|
|
|
return filtered_tags, filtered_weights
|
|
|
|
# Apply transformation to each row
|
|
gold_mod_prices["ProcessedTags"], gold_mod_prices["ProcessedSpawnWeight"] = zip(
|
|
*gold_mod_prices.apply(
|
|
lambda row: process_tags(row["Tags"], row["SpawnWeight"]), axis=1
|
|
)
|
|
)
|
|
|
|
category_expansion = {
|
|
7: {16, 4, 22, 25, 1, 45}, # ARMOUR -> Armour pieces
|
|
8: {5, 12, 11, 15, 1010, 14, 13, 1014, 1012, 566},
|
|
}
|
|
|
|
# Function to apply logical filtering based on category expansion and removal of 0-weighted tags
|
|
def filter_tags_and_remove_category(tags_list, weights_list, category_expansion):
|
|
tag_dict = {tag: bool(weight) for tag, weight in zip(tags_list, weights_list)}
|
|
|
|
# Process category expansions
|
|
expanded_tags = set()
|
|
for tag in tags_list:
|
|
if tag in category_expansion:
|
|
for expanded_tag in category_expansion[tag]:
|
|
if expanded_tag not in tag_dict:
|
|
tag_dict[expanded_tag] = (
|
|
True # Mark new expanded tags as 1 (true)
|
|
)
|
|
expanded_tags.add(tag) # Mark category for removal
|
|
|
|
# Remove tags with weight 0 and also remove the original category tags
|
|
filtered_tags = [
|
|
tag for tag, keep in tag_dict.items() if keep and tag not in expanded_tags
|
|
]
|
|
|
|
return filtered_tags
|
|
|
|
# Apply the filtering to each row
|
|
gold_mod_prices["FinalTags"] = gold_mod_prices.apply(
|
|
lambda row: filter_tags_and_remove_category(
|
|
row["ProcessedTags"], row["ProcessedSpawnWeight"], category_expansion
|
|
),
|
|
axis=1,
|
|
)
|
|
|
|
gold_explode = gold_mod_prices.explode("FinalTags")
|
|
merged_gold_explode = gold_explode.merge(
|
|
tags, left_on="FinalTags", right_index=True, how="left"
|
|
)
|
|
fixed_gold_mod_prices = (
|
|
merged_gold_explode.groupby("Mod")
|
|
.agg(
|
|
{
|
|
"Id": list, # Keep the Tag IDs as lists
|
|
}
|
|
)
|
|
.reset_index()
|
|
.rename(columns={"Id": "Tags"})
|
|
)
|
|
fixed_gold_mod_prices["SpawnWeight"] = gold_mod_prices["ProcessedSpawnWeight"]
|
|
|
|
gold_with_readable_tags_df = pd.merge(
|
|
fixed_gold_mod_prices[
|
|
fixed_gold_mod_prices["Tags"].apply(
|
|
lambda x: not (
|
|
isinstance(x, list)
|
|
and len(x) == 1
|
|
and isinstance(x[0], float)
|
|
and np.isnan(x[0])
|
|
)
|
|
)
|
|
],
|
|
mods,
|
|
left_on="Mod",
|
|
right_index=True,
|
|
how="left",
|
|
)[["Id", "Tags"]]
|
|
gold_with_readable_tags = {
|
|
x["Id"]: x["Tags"] for x in gold_with_readable_tags_df.to_dict(orient="records")
|
|
}
|
|
return gold_with_readable_tags
|
|
|
|
|
|
GENERATION_TYPE = {
|
|
1: "prefix",
|
|
2: "suffix",
|
|
3: "intrinsic",
|
|
4: "nemesis",
|
|
5: "corrupted",
|
|
6: "bloodlines",
|
|
7: "torment",
|
|
8: "tempest",
|
|
9: "talisman",
|
|
10: "enchantment",
|
|
11: "essence",
|
|
12: "unused",
|
|
13: "bestiary",
|
|
14: "delve",
|
|
15: "synthesis unknown",
|
|
16: "synthesis globals",
|
|
17: "synthesis bonus",
|
|
18: "blight",
|
|
19: "blight tower",
|
|
20: "monster affliction",
|
|
21: "enkindling orb",
|
|
22: "instilling orb",
|
|
23: "expedition logbook",
|
|
24: "scourge benefit",
|
|
25: "scourge detriment",
|
|
26: "scourge gimmick",
|
|
27: "unused",
|
|
28: "searing exarch",
|
|
29: "eater of worlds",
|
|
30: "archnemesis",
|
|
31: "crucible passive skill tree",
|
|
32: "crucible passive skill tree mutation",
|
|
33: "affliction wisps",
|
|
34: "necropolis downside",
|
|
35: "necropolis upside",
|
|
}
|
|
|
|
|
|
def modTierBuilderB(mod_data, base_item_types, gold_mod_prices, tags):
|
|
gold_with_readable_tags = get_gold_tags(mod_data, gold_mod_prices, tags)
|
|
|
|
# Flatten mods data for easier processing
|
|
flat_mods = []
|
|
logger.info("Starting to build mods")
|
|
logger.info(f"Incoming data: {len(mod_data)}")
|
|
|
|
for mod in mod_data:
|
|
mod_index = mod.get("_index")
|
|
if mod_index is None:
|
|
continue
|
|
mod_unique_id = mod.get("Id")
|
|
mod_type = mod.get("ModType")
|
|
mod_level = mod.get("Level")
|
|
mod_domain = mod.get("Domain")
|
|
mod_name = mod.get("Name")
|
|
mod_jewel_radius = mod.get("RadiusJewelType")
|
|
gen = mod.get("GenerationType")
|
|
if gen:
|
|
mod_generation = GENERATION_TYPE.get(gen)
|
|
else:
|
|
mod_generation = "unknown"
|
|
mod_stats = []
|
|
mod_stat_ids = []
|
|
mod_allowed_base_types = gold_with_readable_tags.get(mod_unique_id, [])
|
|
if len(mod_allowed_base_types) == 0:
|
|
if mod_unique_id.lower().startswith("jewelradius"):
|
|
mod_allowed_base_types = ["radius_jewel"]
|
|
elif mod_unique_id.lower().startswith("jewel"):
|
|
mod_allowed_base_types = ["jewel"]
|
|
|
|
is_processed_map = False
|
|
if mod_unique_id.lower().startswith("map"):
|
|
stat1 = mod.get("Stat1")
|
|
if stat1 == 1004:
|
|
stat2 = mod.get("Stat2")
|
|
stat2_value = mod.get("Stat2Value")
|
|
|
|
if stat2 == 1202:
|
|
stat3 = mod.get("Stat3")
|
|
stat3_value = mod.get("Stat3Value")
|
|
if (
|
|
stat3 is not None
|
|
and stat3_value is not None
|
|
and isinstance(stat3_value, list)
|
|
):
|
|
mod_stats.append(stat3_value)
|
|
mod_stat_ids.append(stat3)
|
|
is_processed_map = True
|
|
else:
|
|
mod_stats.append(stat2_value)
|
|
mod_stat_ids.append(stat2)
|
|
is_processed_map = True
|
|
if not is_processed_map:
|
|
for stat_index in range(1, 7):
|
|
stat = mod.get(f"Stat{stat_index}")
|
|
if stat is None:
|
|
continue
|
|
stat_value = mod.get(f"Stat{stat_index}Value")
|
|
if stat_value is None or not isinstance(stat_value, list):
|
|
continue
|
|
mod_stat_ids.append(stat)
|
|
if stat == 119:
|
|
mod_stats.append([a / 60 for a in stat_value])
|
|
else:
|
|
mod_stats.append(stat_value)
|
|
|
|
mod_mod = {
|
|
"mod_index": mod_index,
|
|
"mod_unique_id": mod_unique_id,
|
|
"mod_type": mod_type,
|
|
"mod_level": mod_level,
|
|
"mod_domain": mod_domain,
|
|
"mod_name": mod_name,
|
|
"mod_jewel_radius": mod_jewel_radius,
|
|
"mod_stat_values": mod_stats,
|
|
"mod_stat_ids": mod_stat_ids,
|
|
"mod_allowed_base_types": mod_allowed_base_types,
|
|
"mod_generation": mod_generation,
|
|
}
|
|
flat_mods.append(mod_mod)
|
|
|
|
logger.info(f"Created {len(flat_mods)} mods")
|
|
|
|
# step 2
|
|
by_id = {}
|
|
for mod in flat_mods:
|
|
mod_id_with_trailing_num = mod.get("mod_unique_id")
|
|
mod_id = re.sub(r"\d+_*$", "", mod_id_with_trailing_num.strip())
|
|
if mod_id not in by_id:
|
|
by_id[mod_id] = {
|
|
"mods": [],
|
|
"mods_id": mod_id,
|
|
"mod_type": mod["mod_type"],
|
|
"mod_allowed_base_types": set(mod["mod_allowed_base_types"]),
|
|
"mod_stat_ids": set(mod["mod_stat_ids"]),
|
|
"mod_generation": mod["mod_generation"],
|
|
"mod_jewel_radius": mod["mod_jewel_radius"],
|
|
}
|
|
by_id[mod_id]["mods"].append(mod)
|
|
by_id[mod_id]["mod_stat_ids"] = by_id[mod_id]["mod_stat_ids"].union(
|
|
set(mod["mod_stat_ids"])
|
|
)
|
|
by_id[mod_id]["mod_allowed_base_types"] = by_id[mod_id][
|
|
"mod_allowed_base_types"
|
|
].union(set(mod["mod_allowed_base_types"]))
|
|
|
|
logger.info(f"Created {len(by_id)} unique mods")
|
|
|
|
grouped_by_mod_type = defaultdict(list)
|
|
for mod_id, mod_group in by_id.items():
|
|
grouped_by_mod_type[
|
|
mod_group["mod_type"]
|
|
if "radius_jewel" not in mod_group["mod_allowed_base_types"]
|
|
else mod_group["mods_id"]
|
|
].append(mod_group)
|
|
|
|
output_data = []
|
|
|
|
for mod_type, mod_groups in grouped_by_mod_type.items():
|
|
one_id = None
|
|
all_stats = set()
|
|
output_tier = None
|
|
for mod_group in mod_groups:
|
|
# HACK: overwrite for things that are not hybrid that should be included
|
|
if mod_group["mods_id"] not in ("ShockEffectUnique__"):
|
|
all_stats = all_stats.union(mod_group["mod_stat_ids"])
|
|
mod_id = mod_group["mods_id"].lower()
|
|
if one_id is None:
|
|
one_id = mod_id
|
|
if (
|
|
mod_group["mod_generation"] == "suffix"
|
|
or mod_group["mod_generation"] == "prefix"
|
|
) and len(mod_group["mod_allowed_base_types"]) > 0:
|
|
if output_tier is None:
|
|
output_tier = modGroupToOutputTier(mod_group)
|
|
else:
|
|
output_tier.update(modGroupToOutputTier(mod_group))
|
|
# NOTE: just based on the dict with most items in it
|
|
output_data.append((tuple(all_stats), output_tier, one_id))
|
|
logger.info(f"Created {len(output_data)} mod groups.")
|
|
return output_data
|
|
|
|
|
|
def modGroupToOutputTier(mod_group):
|
|
base_counts_dict = {
|
|
base: len(
|
|
[mod for mod in mod_group["mods"] if base in mod["mod_allowed_base_types"]]
|
|
)
|
|
if base != "radius_jewel"
|
|
else mod_group["mod_jewel_radius"]
|
|
for base in mod_group["mod_allowed_base_types"]
|
|
}
|
|
return base_counts_dict
|
|
|
|
|
|
if __name__ == "__main__":
|
|
import json
|
|
|
|
with open(
|
|
f"{os.path.dirname(os.path.realpath(__file__))}/../tables/en/Mods.json",
|
|
"r",
|
|
encoding="utf-8",
|
|
) as f:
|
|
mod_data = json.load(f)
|
|
with open(
|
|
f"{os.path.dirname(os.path.realpath(__file__))}/../tables/en/BaseItemTypes.json",
|
|
"r",
|
|
encoding="utf-8",
|
|
) as f:
|
|
base_item_data = json.load(f)
|
|
with open(
|
|
f"{os.path.dirname(os.path.realpath(__file__))}/../tables/en/GoldModPrices.json",
|
|
"r",
|
|
encoding="utf-8",
|
|
) as f:
|
|
gold_data = json.load(f)
|
|
with open(
|
|
f"{os.path.dirname(os.path.realpath(__file__))}/../tables/en/Tags.json",
|
|
"r",
|
|
encoding="utf-8",
|
|
) as f:
|
|
tags = json.load(f)
|
|
logging.basicConfig(level=logging.INFO)
|
|
data = modTierBuilderB(mod_data, base_item_data, gold_data, tags)
|
|
for keys, mods, id in data:
|
|
if len(mods["explicit"]) > 1:
|
|
x = mods["explicit"]
|
|
print(x)
|