Item Generation (Modifier Rolling)¶
Summary: When an equipment item is created,
UItemGenerationLibrarypopulates itsFEquipmentFragmentwith rolled modifiers. Generation runs in a fixed order — implicit(s) → prefixes → suffixes — and (for weapons) appends weapon scaling. Explicit affixes (prefix/suffix) are chosen by weighted random from the base type's modifier pool, gated by a per-base-type spawn weight; implicits are NOT weighted — they are listed explicitly on the base type (ImplicitModifierIDs) and fetched by ID. Each rolled explicit modifier gets a tier (random among tiers unlocked at the item level) and a value rolled inside that tier's range; implicits are PINNED to the highest tier unlocked at the item level (FSH-364 ruling, 2026-07-23) — deterministic base signatures, only the value rolls. The entire pipeline can be driven by anFRandomStream, so the editor Roll Simulator and PIE produce identical items from the same seed.
Table of Contents¶
- Design Status (settled 2026-07-23)
- Architecture Overview
- Core Concepts
- The Generation Pipeline (incl. Weapon Damage Curve & Quality Roll)
- Affix Selection & Spawn-Weight Gating
- Implicits Are Fetched by ID, Not Weighted
- Tier Selection
- Determinism & Seeding
- Historical Confusion (resolved): the "+1" Base Attribute Line
- Authoring Conventions (Modifier Pool)
- Public Contracts
- Source References
- Related Systems
- Recent Changes
Design Status (settled 2026-07-23)¶
The Base Attribute layer is REMOVED (TestPass-D2 ruling).
FItemBaseTypeDefinitionno longer has aBaseAttributesmap; the equip-time multi-attribute GE, tooltip line, stat-source rows, and the Item Manager editor section are all gone. Armor bases carry their armor through thearmor_implicitpool definition instead (theirImplicitModifierIDslist exactly that one entry, so the single-pick implicit channel always yields it). Weapon "+1" base attributes (Ferocity/Clarity/Dread) were vestigial duplicates of existing implicits and were deleted without replacement.Implicit tiers are PINNED (TestPass-D1 ruling): an implicit always takes the highest tier unlocked at the item level (
CalculateHighestTierFromLevel), consuming no randomness; explicit affixes keep the random-among-unlocked roll.
Architecture Overview¶
┌──────────────────────────────────────────────────────────────────────┐
│ UItemGenerationLibrary::GenerateItemProperties(Manifest, Pool, Stream)│
│ │
│ Equipment fragment present? ── no ──▶ return (nothing to roll) │
│ │ yes │
│ ▼ │
│ Resolve base type by BaseTypeID ──▶ FItemBaseTypeDefinition │
│ │ │
│ ├─ bSupportsModifiers && PoolManager ──▶ GenerateModifiers │
│ │ │
│ └─ ItemClass == Weapon ──▶ ApplyWeaponScaling (own sub-seed) │
└──────────────────────────────────────────────────────────────────────┘
│
▼
┌──────────────────────────────────────────────────────────────────────┐
│ GenerateModifiers → GenerateModifiersOfType (× 3, fixed order) │
│ │
│ IMPLICIT ┐ PREFIX ┐ SUFFIX ┐ (shared ExistingFamilies set │
│ │ │ │ threads across all three to │
│ ▼ ▼ ▼ block family duplicates) │
│ ┌─────────────────┐ ┌──────────────────────────────────────────┐ │
│ │ Fetch by ID: │ │ Weighted random from pool: │ │
│ │ ImplicitModi- │ │ GetFilteredModifiers (type/level/tags/ │ │
│ │ fierIDs + level │ │ family) → SelectWeightedRandomSeeded │ │
│ │ window check. │ │ (spawn-weight gate) │ │
│ │ NO weight gate. │ │ │ │
│ └────────┬────────┘ └────────────────────┬─────────────────────┘ │
│ └─────────────────┬────────────────┘ │
│ ▼ │
│ Per pick: ModifierTier = CalculateTierFromLevelSeeded(level, stream) │
│ RolledValue = GetRandomValueFromTier(tier, stream) │
│ ▼ │
│ Append to Implicits / Prefixes / Suffixes on FEquipmentModifiers │
└──────────────────────────────────────────────────────────────────────┘
Core Concepts¶
Why Generation Exists¶
Generation turns a base type (the "what" — a Bastard Sword, a Plate Helm) into a rolled item (the "how good / what flavor"). It is the on-ramp to the Loot System and the foundation the Crafting System mutates. Per the project's no-rarity stance, the modifiers themselves carry an item's identity, so getting their selection and tiering right — and reproducible — is the system's whole job.
The Three Modifier Channels¶
| Channel | Source | Selection | Removable by crafting |
|---|---|---|---|
| Implicit | Base type's ImplicitModifierIDs (explicit list) |
Fetched by ID, level-window check only — no spawn weight | No (granted by base) |
| Prefix / Suffix | Base type's modifier pool(s) | Weighted random, spawn-weight gated | Yes (craftable affixes) |
| Scaling | WeaponScalingLibrary (weapons only) |
Weighted by scaling rank | System-managed |
Echo and Unique modifiers do not flow through this pipeline. Echo mods are rolled by the Remnant factory onto
FRemnantItemState; unique mods are item-specific.GenerateModifiersOfTypeensure-rejects any type other than Implicit/Prefix/Suffix. See Remnant System.
The Generation Pipeline¶
GenerateItemProperties is the single entry point used by every item-creating system (ItemSpawner, vendors, the editor sim subsystem). It:
- Bails immediately for non-equipment items (no
FEquipmentFragment). - Resolves the base type from
BaseTypeID; logs an error and bails if missing. - Stamps
EquipmentFragment->BaseTypeID. - If the base type
bSupportsModifiersand a pool manager is supplied, runsGenerateModifiers. - For weapons (
ItemClassmatchesWeapon), runsApplyWeaponScaling. - For weapons, rolls the per-drop damage quality onto the
FWeaponFragment(see Weapon Damage Curve & Quality Roll). Drawn last so adding this axis never shifted the modifier/scaling draws of a pre-existing seed.
GenerateModifiers then calls GenerateModifiersOfType three times in a fixed order — Implicit, Prefix, Suffix — passing a single ExistingFamilies array by reference through all three. Each successful roll adds its ModifierFamily to that set, so a later channel can never duplicate a family already taken by an earlier one (family-conflict avoidance across the whole item).
Modifier Count¶
The number rolled per channel comes from UModifierLibrary::DetermineModifierCountSeeded:
| Channel | Count rule |
|---|---|
| Implicit | Max 1 (GetMaxModifierCount returns 1) |
| Prefix / Suffix | RandRange(1, MaxCount) where MaxCount is 1 (iLvl 1–34), 2 (35–69), 3 (70+) |
If the count resolves to 0 or the filtered candidate set is empty, that channel produces nothing.
Weapon Scaling Sub-Seed¶
WeaponScalingLibrary is seed-based (takes an int32), not stream-based. To keep the whole item deterministic under one stream, GenerateItemProperties derives a scaling sub-seed from the stream (RandomStream->RandRange(1, MAX_int32-1)); a null stream passes 0, which keeps the unseeded global path.
Weapon Damage Item-Level Curve & Quality Roll¶
Weapon damage has two axes beyond the authored manifest value (added 2026-07-24, feature/itemization-fsh-369-364):
Item-level curve. A manifest's BaseDamage is authored as the on-curve value at the config's anchor item level (currently 5 — where the slice's weapons were tuned). At damage-calculation time, UWeaponPropertiesLibrary::CalculateWeaponDamage multiplies by
with growth/exponent mirroring the enemy MaxHealth curve in EnemyScaling.json, so player damage tracks enemy HP by construction (flat TTK across levels). At current values, iLvl 1 lands ~0.5x and iLvl 40 ~5.9x. The curve lives in Config/Balance/ItemScaling.json, loaded by FItemScalingConfig — a static loader, deliberately not a subsystem, because the same curve must serve runtime generation, the editor Roll Simulator, and the BuildSweep commandlet (contexts without a shared GameInstance). Missing/unreadable config degrades to identity (multiplier 1, variance 0) with a one-time warning. Eternal.Balance.ReloadItemScaling re-reads it live.
Quality roll. At generation, each weapon draws a DamageQualityRoll in [1-v, 1+v] (qualityVariance, currently 0.15) stored on the FWeaponFragment — the "same base, higher roll" axis: two drops of the same base at the same item level differ by up to ±15% base damage. Seeded like everything else (same seed, same roll). Items generated before the axis existed keep the field default 1.0 and are unaffected.
Both multipliers are applied inside CalculateWeaponDamage — the one builder every consumer (tooltip, ExecCalc, Build Lab, sweep) reads — so displayed and dealt damage can never disagree.
Affix Selection & Spawn-Weight Gating¶
Explicit affixes (prefix/suffix) are chosen by weighted random without replacement from the base type's pool. The candidate set is built by UModifierPoolManager::GetFilteredModifiers, a four-stage chain:
All pool definitions
│ FilterByType keep only this channel (Prefix or Suffix)
▼ FilterByItemLevel keep MinimumItemLevel ≤ iLvl ≤ MaximumItemLevel (0 = no cap)
▼ FilterByTags keep only mods with a POSITIVE spawn weight for this base type's tags
▼ FilterByFamilyConflicts drop families already taken by an earlier channel
▼
Filtered candidates ──▶ SelectWeightedRandomSeeded
The Spawn-Weight Gate¶
A modifier's SpawnWeights is a list of (ItemTag, Weight) entries. The gate (DoesItemMatchModifierTags) admits a modifier only if at least one of its spawn-weight tags is present on the base type and carries Weight > 0. The same data then sets relative frequency:
| Step | Function | Effect |
|---|---|---|
| Eligibility | DoesItemMatchModifierTags |
Mod with no positive-weight matching tag is filtered out entirely — it can never roll on this base type |
| Frequency | CalculateSpawnWeight → CalculateWeightFromTags |
Sums the weights of all matching tags; higher total = more likely to be picked |
| Pick | SelectFromWeightedListInternal (stream-aware) |
Linear weighted draw over the summed weights; picked mod is removed (no replacement), then the next pick draws again |
If, after filtering, every candidate has zero weight, selection falls back to uniform over the candidates so an item is never left empty. With no item tags supplied to the selector (the runtime case), selection is effectively uniform among the already-tag-filtered candidates.
Implicits Are Fetched by ID, Not Weighted¶
Implicit modifiers are authored on the base type, in FItemBaseTypeDefinition::ImplicitModifierIDs (a plain TArray<FString> of modifier IDs). They intentionally carry no SpawnWeights — selection is by explicit list, not by the weighted pool.
This matters because of how the two paths differ:
PREFIX / SUFFIX: GetFilteredModifiers → FilterByTags drops anything with no
positive spawn weight → SelectWeightedRandomSeeded
IMPLICIT: for each ID in base.ImplicitModifierIDs:
def = PoolManager->FindModifierByID(ID)
keep if MinimumItemLevel ≤ iLvl ≤ MaximumItemLevel
(NO spawn-weight gate — these have none by design)
If implicits were routed through the weighted path, FilterByTags would drop every one of them (they have no positive spawn weight), and the item would silently lose its implicit. That is exactly the bug fixed in 72a7975ed: after a content re-author, the implicit branch was still weight-gating, so no item rolled an implicit at all. The fix added the dedicated by-ID branch in GenerateModifiersOfType (with a level-window check only).
Authoring rule: an implicit must (a) be listed in the base type's
ImplicitModifierIDs, and (b) resolve viaFindModifierByIDagainst a loaded pool (item pool, dungeon, echo, or auxiliary — all are indexed in the manager's master lookup). It must not rely on spawn weights.
Tier Selection¶
Every rolled modifier (implicit or explicit) is assigned a tier, then a value rolled inside that tier's range:
ModifierTier = PoolManager->CalculateTierFromLevel(iLvl, stream)
RolledValue = Definition->GetRandomValueFromTier(ModifierTier, stream)
│ uses TiersValueRanges[Tier] (per-tier Min..Max)
How the Tier Is Picked¶
UModifierPoolDataAsset::TierMap maps EModifierTier → minimum item level. For explicit affixes, CalculateTierFromLevelSeeded collects every tier whose threshold the item level meets, then picks randomly among the unlocked tiers (not the highest). Same item level can therefore yield different tiers across rolls — but the same seed yields the same tier.
Implicits do not roll a tier. The implicit channel passes bPinHighestTier into SelectRandomModifiersSeeded, which resolves CalculateHighestTierFromLevel — the best (lowest-enum) tier unlocked at the item level, a pure function of item level that consumes no stream draw. The Item Composer, Build Lab validation, and the composition specs all mirror the pin: an implicit can only be authored/seated/retiered at exactly the pinned tier, and BuildLoadoutValidation flags implicit values from any other tier's range.
Which Pool Decides the Tier¶
The pool manager loads several pools, and not all of them define tiers. UModifierPoolManager::CalculateTierFromLevel walks LoadedPools and uses the first pool with a non-empty TierMap — not simply the first pool:
| Pool kind | Has TierMap? | Decides tier? |
|---|---|---|
| Item loot pool | Yes | ✅ first non-empty wins |
| Sealed Echo / Greed (blessing) pools | Empty | ❌ skipped |
Sealed pools are scanned under the same primary asset type but carry empty TierMaps, and load order differs between the runtime and editor-sim managers. Selecting "first pool" blindly (the pre-72a7975ed behavior) was order-dependent and could pick a tier-less pool. If no loaded pool has a TierMap, the manager logs a warning and falls back to Tier1.
Not Every Modifier Has Tiers: Granted-Ability-Only Procs¶
Tiering assumes a modifier has a rolled value — a TargetProperty attribute plus a TiersValueRanges map. Granted-ability-only proc affixes do not. Introduced with the proc-affix pass (989bb8d9f, ignite-on-crit; extended by the 2026-07-02 pass), this modifier shape sets FModifierDefinition::GrantedAbility (an ability granted to the ASC on equip, removed on unequip) and carries no TargetProperty and no TiersValueRanges:
| Field | Normal (rolled) modifier | Granted-ability-only proc |
|---|---|---|
TargetProperty |
Set (an attribute in TagsToAttributes) |
Empty |
TiersValueRanges |
Per-tier Min..Max, rolled | Empty — no value rolled |
GrantedAbility |
None | Set (e.g. GA_OnHit_IgniteOnCrit, GA_OnBlock_Bleed) |
Because there is no value axis, these mods are not tiered — GenerateModifiersOfType still stamps a ModifierTier from the pool, but GetRandomValueFromTier has no range to read, so no magnitude is applied; the ability's own logic (guaranteed-on-condition, e.g. ignite on crit / bleed on block) is the whole effect. The scaling axis is the trigger's uptime (crit rate, block uptime), not a rolled number.
The itemization import validator (UModifierPoolValidator) skips its tier-coverage check for this shape — the guard is !TargetProperty.IsValid() && GrantedAbility — so a proc affix with an empty TiersValueRanges is not flagged as "rolls 0.0". The ImportJson authoring path likewise admits an entry with no TargetProperty as long as GrantedAbility is set.
Determinism & Seeding¶
The whole pipeline accepts an optional FRandomStream*. Null stream = global RNG (FMath::RandRange); a supplied stream makes every random decision — count, affix pick, tier, value, weapon-scaling sub-seed — reproducible.
| Caller | Stream? | Result |
|---|---|---|
UItemSpawner (live drops, vendors) |
Null | Global RNG — each drop is fresh |
UEternalCheatManager::GiveItem <ID> [iLvl] [Seed] |
FRandomStream(Seed) when Seed ≠ 0 |
Reproducible item from a seed |
Editor Roll Simulator (UEditorItemSimSubsystem) |
Seeded stream | Sim output equals in-game output for the same seed |
This is the contract that makes the Itemization Tooling credible: the simulators call the runtime generation code, seeded with an FRandomStream, rather than reimplementing it — so a Roll Simulator result and a PIE roll from the same seed are identical. Before 72a7975ed, tier selection used unseeded FMath::RandRange even on the seeded path, so same-seed editor-sim vs PIE values mismatched; CalculateTierFromLevelSeeded closed that desync.
The Send-to-PIE path (
GiveSimulatedItem) transfers an already-rolled manifest rather than re-rolling from a seed, so the item the designer simulated is the exact item that spawns. See Send-to-PIE.
Historical Confusion (resolved): the "+1" Base Attribute Line¶
(FSH-312 recurring QA misread — the layer that caused it is now deleted.) Items used to show a static
+NBase Attribute line (fromFItemBaseTypeDefinition::BaseAttributes) directly above the rolled implicit, which QA read as "the implicit only rolls +1". The 2026-07-23 rulings removed the layer entirely and pinned implicit tiers, so an item now shows only rolled modifiers: a pinned-tier implicit plus random-tier explicit affixes. If a stray "+1"-style line ever reappears, it is a regression, not a design layer.
Authoring Rule: Weapon Damage Modifiers Are Local (2026-07-01)¶
Flat and Increased weapon damage affixes only function on the weapon itself (PoE-style local
modifiers). CalculateWeaponDamageBreakdown reads the attacking weapon's own
FEquipmentFragment::GetLocalModifiers() — a "+18 Fire Damage" roll on a ring would display in the
tooltip and silently do nothing to attacks. Do not author flat/local weapon-damage affixes into
non-weapon modifier pools.
Non-weapon slots express offense through the global families instead: Stats.Offensive.Increased.*
(per-type Increased %), crit chance/damage, ailment chance, Stats.Offensive.AilmentDealt.*, and
conditional-vs-status. Those are attribute-backed and work from any slot. If "caster gloves" style
global flat damage is ever wanted, it needs new Flat*Damage attributes folded into the breakdown
builder — a deliberate feature, not a pool-authoring change (decision recorded 2026-07-01: local-only
for MVP; GDD Modifier Anatomy carries the same rule).
Authoring Conventions (Modifier Pool)¶
Migrated from the retired
Tools/Python/modifier_content_table.pydocstring (2026-07-02). The pool's full state now lives inTools/ModifierPool.json(export/sync workflow — see Itemization Tooling); these are the rules that keep new entries functional, not just well-formed.
Tier Thresholds & the UNIQUE-Tier Trap¶
The item pool's TierMap is Tier5 @ ilvl 1, Tier4 @ 15, Tier3 @ 30, Tier2 @ 45, Tier1 @ 60.
Never author a Unique threshold into TierMap. Tier selection picks randomly among unlocked
tiers (see Tier Selection), so a Unique: 100 row would let ilvl-100 items roll
tier Unique — and any modifier without a UNIQUE entry in its TiersValueRanges then rolls 0.0
(engine trap; this was live in the pre-content-sprint data). Legacy attribute mods keep a dormant
UNIQUE: 100–200 value range for unique-item generation — that is the only sanctioned use of the
Unique tier in ranges.
Operation Conventions per Attribute Bucket¶
Derived from EternalAttributeSet TagsToAttributes and the DamageBuckets/ConditionalModifiers seam
(4f8048c2d):
| Bucket | Scope / Op | Why |
|---|---|---|
Stats.Offensive.Increased.* |
Global / Flat | The attribute itself is the additively-summed percentage — an Increased op would double-apply |
ConditionalDamage.Vs* |
Conditional / Flat, empty WearerTagRequirements |
These gate on the victim's Status.* tags in ExecCalc_Damage; wearer reqs are for wearer-state conditionals only (RequireTags must all be present, IgnoreTags all absent) |
Multiplier attributes (DamageDealt, DamageTaken, StaminaCostMultiplier) |
Global / Increased | GAS multiplicative bias-1.0 SumMods; a negative roll = reduction |
Local-Scope Routing: Damage Is Field-Based, Poise/Stamina Still String-Matched¶
As of 2026-07-24 (feature/balance-curve-retune), the damage families route through explicit
definition fields — Scope / Operation / TargetProperty / ModifierFamily
(IsLocalFlatDamageModifier / IsLocalDamagePercentModifier in WeaponPropertiesLibrary) — and the
old DoesModifierAffectStat("Damage") description-inspection path is retired for them. Per-type
damage lanes compute (curve-scaled base + local flats) × local %-increased stack: flat joins the
base before percent multiplies, so flat + percent is a combo, not competitors. All 8 damage-type
families participate.
String inspection survives for the non-damage local stats (poise, stamina cost), so wording there is still load-bearing (cleanup tracked as its own card):
- The poise mod's family/description must avoid the substring
Damage/damageor it leaks into the weapon Damage stat — hence familyPoisePercentand the wording "Poise Break". - Local percent mods must have
Percentin theModifierFamilyname.
Flat Weapon-Damage Tiers Are Fraction-Baked¶
Flat damage affix tiers are authored as fractions of curve-scaled base damage, then baked into
the absolute TiersValueRanges players see — the affix's share of a hit holds constant across the
descent instead of decaying into noise:
Field (on FModifierDefinition) |
Role |
|---|---|
TierValueFractions |
TMap<EModifierTier, FFloatInterval> — per-tier min/max fraction of reference base (authoring source of truth) |
FractionReferenceBaseDamage |
Reference weapon base damage the fractions multiply (e.g. 13 for 1H, 21 for 2H families) |
The bake (Eternal.Modifiers.BakeFlatDamageTiers console command) computes each tier's bracket base
as reference × item-level curve at the tier's TierMap unlock level and writes the absolute ranges
(min width: a 2-value range). Rolled items store absolute values as before — nothing at
generation/runtime reads fractions; the bake is an authoring-time step, re-run when fractions, the
reference, or the curve change. FlatDamageBakeContract.spec recomputes the baked ranges through the
same loaders. Stored loadout/preset rolls survive a re-bake via
Eternal.Modifiers.MigrateFlatDamageRolls (snap-to-nearest-baked-edge, never re-roll);
Eternal.Modifiers.FixIllegalAuthoring repairs authored rolls/implicits that violate tier legality.
Do not hand-edit
TiersValueRangeson fraction-authored definitions — the ranges are derived, and the next bake clobbers the edit. Edit the fraction and re-bake. Enforced: the content validator (on-save / pre-push / CI) errors when a baked range disagrees with its authored fraction, naming the fix command;FlatDamageBakeContract.specpins the same contract in the test suite.
Negative-Roll Wording¶
Word descriptions without "reduced" so the minus sign reads naturally ("-14% Stamina Cost of
Attacks") — FormatDescription performs no abs() on the rolled value.
Public Contracts¶
UItemGenerationLibrary¶
| Method | Parameters | Purpose |
|---|---|---|
GenerateItemProperties |
(FItemManifest&, UModifierPoolManager*, FRandomStream* = nullptr) |
Main entry point: rolls modifiers + weapon scaling onto an equipment manifest |
GenerateModifiers |
(FEquipmentFragment&, const FItemGenerationParams&, UModifierPoolManager*, FRandomStream*) |
Rolls implicit → prefix → suffix with shared family tracking |
ApplyWeaponScaling |
(FEquipmentFragment&, const FItemBaseTypeDefinition&, int32 ItemLevel, int32 RandomSeed = 0) |
Appends weapon scaling modifiers (seed-based; 0 = global) |
UModifierPoolManager¶
| Method | Parameters | Purpose |
|---|---|---|
GetFilteredModifiers |
(ItemTags, iLvl, ModifierType, ExistingFamilies) |
Type → level → tag (spawn-weight) → family filter chain |
SelectRandomModifiersSeeded |
(FilteredPool, Count, iLvl, FRandomStream*) |
Weighted draw without replacement; assigns tier + value per pick |
CalculateTierFromLevel |
(iLvl, FRandomStream* = nullptr) |
Picks tier from the first pool with a non-empty TierMap |
FindModifierByID |
(ModifierID) |
Master-lookup resolve (used by the implicit-by-ID branch) |
UModifierPoolDataAsset¶
| Member | Type | Purpose |
|---|---|---|
TierMap |
TMap<EModifierTier, int32> |
Tier → minimum item level (empty on sealed pools) |
CalculateTierFromLevelSeeded |
(iLvl, FRandomStream*) |
Random tier among those unlocked at iLvl; null stream = global RNG |
Key Data Fields¶
| Field | On | Role |
|---|---|---|
ImplicitModifierIDs |
FItemBaseTypeDefinition |
Explicit implicit list (by-ID, no spawn weight). The channel rolls one pick from this list — a base wanting a guaranteed line (e.g. armor bases → armor_implicit) must list exactly that one entry |
SpawnWeights |
FModifierDefinition |
Per-tag eligibility + frequency gate for explicit affixes |
TiersValueRanges |
FModifierDefinition |
Per-tier Min..Max for the rolled value — empty on granted-ability-only procs |
GrantedAbility |
FModifierDefinition |
Ability granted on equip (proc affix). When set with no TargetProperty, the mod rolls no value and is not tiered |
Source References¶
| Class / Concept | Location |
|---|---|
| Generation entry + per-channel rolling | Private/Inventory/Items/Generation/ItemGenerationLibrary.cpp |
Generation public API + FItemGenerationParams |
Public/Inventory/Items/Generation/ItemGenerationLibrary.h |
| Filter chain, weighted selection, count, spawn-weight math | Private/Inventory/Modifiers/ModifierLibrary.cpp |
| Pool manager (filter / select / tier source / lookup) | Private/Inventory/Modifiers/ModifierPoolManager.cpp, Public/Inventory/Modifiers/ModifierPoolManager.h |
Tier roll + TierMap |
Private/Inventory/Data/ModifierPoolDataAsset.cpp, Public/Inventory/Data/ModifierPoolDataAsset.h |
FModifierDefinition, FRolledModifier, FEquipmentModifiers, enums |
Public/Inventory/Modifiers/ModifierDefinitions.h |
ImplicitModifierIDs, BaseAttributes, base type def |
Public/Inventory/Data/ItemBaseTypeDataAsset.h |
| Live-drop caller (null stream) | Private/Inventory/Items/ItemSpawner.cpp |
| Seeded / send-to-PIE callers | Private/Debug/EternalCheatManager.cpp |
| Editor seeded reuse | Private/Shared/Simulation/EditorItemSimSubsystem.cpp (ProjectEternalEditor) |
Related Systems¶
- Item System — manifests, fragments, the item object lifecycle generation feeds
- Loot System — what selects base types / item levels and triggers generation on drop
- Crafting System — mutates already-generated affixes (reroll / add / remove, influence reagents)
- Conditional Modifiers — modifier-definition variants that roll through this same pipeline
- Itemization Tooling — Roll Simulator / Send-to-PIE; same seeded code path
Recent Changes¶
| Date | Change | Impact |
|---|---|---|
| 2026-07-24 | Flat weapon affixes rejoin the damage builder + fraction-baked tiers (FSH-435, feature/balance-curve-retune) |
Damage-family local mods route on explicit Scope/Operation/TargetProperty/Family fields (string inspection retired for damage; survives for poise/stamina). Per-type lanes = (curve base + local flats) × local %-increased. Flat tiers authored as TierValueFractions × FractionReferenceBaseDamage, baked to absolute ranges via Eternal.Modifiers.BakeFlatDamageTiers; migration (MigrateFlatDamageRolls) + authoring repair (FixIllegalAuthoring) commands; FlatDamageBakeContract.spec guards the bake. See fraction-bake section |
| 2026-07-24 | Weapon item-level damage curve + per-drop quality roll (feature/itemization-fsh-369-364) |
New curve & quality section: manifest BaseDamage = on-curve value at the anchor item level (5); CalculateWeaponDamage multiplies by the ItemScaling.json curve (enemy-HP shape, iLvl 40 ≈ 5.9x) and the weapon's seeded DamageQualityRoll (±15%, drawn last in generation so pre-existing seeds keep their draws; pre-existing items default 1.0). FItemScalingConfig static loader + Eternal.Balance.ReloadItemScaling |
| 2026-06-17 | Initial authoring of the item-generation / modifier-rolling doc | Documents the shipped pipeline (implicit-by-ID → weighted prefix/suffix → seeded tier/value) and the 72a7975ed fix: implicits were silently weight-gated to nothing (now fetched by ImplicitModifierIDs), and seeded tier selection used unseeded RNG (now CalculateTierFromLevelSeeded, with the tier source being the first pool with a non-empty TierMap). Includes the FSH-312 "+1 is the Base Attribute line, not the implicit" clarification and a Design-Status caveat that the base-attribute layer may be removed. |
| 2026-07-23 | Implicit tier pinning + Base Attribute layer removal (FSH-364 / TestPass-D1+D2, feature/itemization-fsh-369-364) |
Implicits pin to the highest tier unlocked at the item level (CalculateHighestTierFromLevel, no stream draw); explicit affixes keep the random roll. BaseAttributes deleted end-to-end (struct field, equip GE, tooltip, stat sources, Item Manager section); armor bases fold their armor into the new armor_implicit (sole entry in their ImplicitModifierIDs); vestigial weapon/shield "+1" lines deleted without replacement. Composer/Build-Lab/Roll-Sim mirrors updated. |
| 2026-07-02 | Granted-ability-only proc affixes documented (989bb8d9f) |
Added Not Every Modifier Has Tiers: the proc-affix shape sets GrantedAbility with no TargetProperty and no TiersValueRanges, so it rolls no value and is not tiered (guaranteed-on-condition; e.g. ignite-on-crit, bleed-on-block). The import validator skips tier-coverage for it (!TargetProperty.IsValid() && GrantedAbility); GrantedAbility added to the Key Data Fields table. |