seals and charms#763
Conversation
ProfileModel rejected Seals and Charms because those sections were not defined. TTS could classify HoradricSeal/Charm, but read_descr treated them as unsupported non-equipment and returned None. Changed files: src/config/profile_models.py src/item/data/item_type.py src/item/descr/read_descr_tts.py src/item/filter.py tests/config/models_test.py tests/item/read_descr_tts_test.py tests/item/filter/filter_test.py
Charms and Seals shared a generic spellcraft filter model, so charm identity fields and seal slot count could not be represented or matched. Charm set data also was not exposed on parsed Items. Changed files: src/config/profile_models.py: added separate CharmFilterModel and SealFilterModel. src/item/filter.py: added charm set/uniqueAspect matching and seal slotCount matching. src/item/descr/read_descr_tts.py, src/item/models.py, src/dataloader.py: parse/store charm set names using sets.json. Importer/editor files updated to create the correct charm/seal model types. Targeted tests updated for model validation, matching, and charm set parsing.
Root cause:
Seals only supported slotCount; there was no separate field for “boosts this set”. Seal TTS also did not store the detected boosted set on the parsed Item.
Changed files:
src/config/profile_models.py: added SealFilterModel.boostedSet, validated against sets.json.
src/item/models.py: added boosted_set_name.
src/item/descr/read_descr_tts.py: set detection now supports both charms and seals; seals populate boosted_set_name.
src/item/filter.py: seal matching now checks boostedSet alongside existing criteria.
Updated targeted tests in tests/config/models_test.py, tests/item/filter/filter_test.py, tests/item/read_descr_tts_test.py.
Seals:
- CrucibleFury:
boostedSet: berserkers_crucible
affixPool:
- count:
- maximum_fury
cjshrader
left a comment
There was a problem hiding this comment.
I know you're still working on it, I see there are some unaddressed comments but you're not done.
| def _get_aspect_from_tts_section(tts_section: list[str], item: Item, start: int, num_affixes: int): | ||
| # Grab the aspect as well in this case | ||
| if item.rarity in [ItemRarity.Mythic, ItemRarity.Unique, ItemRarity.Legendary]: | ||
| if item.rarity in [ItemRarity.Mythic, ItemRarity.Unique, ItemRarity.Legendary] and not is_seal_or_charm( |
There was a problem hiding this comment.
Unique charms too. This code would skip them
There was a problem hiding this comment.
read_descr_tts.py
— removed the is_seal_or_charm exclusion so aspects are parsed for all Mythic/Unique/Legendary items regardless of type.
update14
cjshrader
left a comment
There was a problem hiding this comment.
Please review all of your other test data and make sure it's real. I haven't been able to review everything
| "chance_when_struck_to_gain_life_as_barrier_for_seconds": "chance when struck to gain life as barrier for seconds", | ||
| "charge_cooldown_reduction": "charge cooldown reduction", | ||
| "charge_damage": "charge damage", | ||
| "charm_slot": "charm slot", |
There was a problem hiding this comment.
If we don't have charm affix data in our data, I'd rather see if we can update gen_data to find it as this is surely not a comprehensive list.
I'm ok if charm/seal affix data goes in its own file. That'll make the using the profile editor easier. I'm ok if both even end up in their own individual files if they don't overlap.
There was a problem hiding this comment.
To be clear I'd prefer it if they could be in their own files, if possible
| (3393, 911), # Bottom-Right | ||
| (3126, 1066), # Bottom (Locked in screenshot) | ||
| (2865, 915), # Bottom-Left | ||
| (2861, 622), # Top-Left |
There was a problem hiding this comment.
Ah cool, so this is where this stuff is
| DynamicItemFilterModel = RootModel[dict[str, ItemFilterModel]] | ||
|
|
||
|
|
||
| class SealCharmFilterModel(BaseModel): |
There was a problem hiding this comment.
Make sure we keep everything alphabetical, this should be lower in the file
| @field_validator("set_name") | ||
| @classmethod | ||
| def set_must_exist(cls, name: str | None) -> str | None: | ||
| return _normalize_existing_set_name(name, "set") |
There was a problem hiding this comment.
unique_aspect is only normalized here; it is never checked against Dataloader().aspect_unique_dict. An editable combo can still save an invalid value, and the filter will silently never match. This should mirror the set_name validator.
| @staticmethod | ||
| def _match_charm_filter(item: Item, filter_spec: CharmFilterModel) -> bool: | ||
| identity_fields = [filter_spec.set_name, filter_spec.unique_aspect] | ||
| if not any(identity_fields): |
There was a problem hiding this comment.
If both set_name and unique_aspect are populated, this returns true when either one matches. That makes the filter broader than the editor implies; if users can fill both fields, it should probably require both to match (or the model should forbid setting both at once).

ProfileModel rejected Seals and Charms because those sections were not defined. TTS could classify HoradricSeal/Charm, but read_descr treated them as unsupported non-equipment and returned None. Changed files:
src/config/profile_models.py
src/item/data/item_type.py
src/item/descr/read_descr_tts.py
src/item/filter.py
tests/config/models_test.py
tests/item/read_descr_tts_test.py
tests/item/filter/filter_test.py