Crafting System¶
Summary: Crafting is a server-authoritative, cube-based deterministic-influence system. Players place a target item plus crafting materials into the crafting cube (
UCraftingContainerComponent, a spatial-grid container). Cube contents are matched against authored recipes; on execute, the server aggregates the materials' affix influence into anFRecipeAffixContext(a guaranteed-specific modifier, guaranteed/biased modifier families, and a bias multiplier), captures that context before consuming the ingredients, then threads it into the recipe outcome and each statelessUCraftingOperation. Material tooltips resolve a{MODIFIER_EFFECT_RANGE}token against the referenced modifier's tier ranges so a shard's authored description shows the real value range it would grant.
Table of Contents¶
- Architecture Overview
- Core Concepts
- Affix Influence & Context Capture
- Recipe Outcomes & Operations
- Material Tooltip Range Resolution
- Pool Resolution
- Public Contracts
- Source References
- Related Systems
- Recent Changes
Architecture Overview¶
┌──────────────────────────────────────────────────────────────────┐
│ CRAFTING CUBE (player-facing) │
│ UCraftingContainerComponent : UItemContainerComponent │
│ • Spatial grid holds target item + materials │
│ • OnCubeContentsChanged → CheckCurrentRecipe │
└──────────────────────────┬───────────────────────────────────────┘
│ contents change
▼
┌──────────────────────────────────────────────────────────────────┐
│ RECIPE MATCHING (UCraftingSubsystem) │
│ • Match cube contents → FCraftingMatchResult │
│ • Validate (ECraftingValidationResult) │
└──────────────────────────┬───────────────────────────────────────┘
│ Server_ExecuteCraft (server RPC)
▼
┌──────────────────────────────────────────────────────────────────┐
│ CRAFT EXECUTION (server-authoritative) │
│ │
│ 1. GetMaterialInfluence() ── builds FRecipeAffixContext │
│ from materials STILL IN the cube │
│ │ │
│ 2. ConsumeIngredients() ── materials now gone │
│ │ │
│ 3. ApplyOutcome(Outcome, Recipe, Context) │
│ │ │
│ ▼ per outcome type │
│ CreateNew / ModifyExisting / TransformItem │
│ │ │
│ ▼ per operation │
│ UCraftingOperation::Execute(Item, Context, PoolManager) │
│ │ │
│ 4. Client_OnCraftingComplete (result modifiers via RPC) │
└──────────────────────────────────────────────────────────────────┘
Key Design Principles¶
| Principle | Implementation |
|---|---|
| Server authority | Server_ExecuteCraft is a Server, Reliable RPC; all consume/apply logic runs on the server. Results pushed to the owning client via Client_OnCraftingComplete. |
| Reuse, don't fork | The cube is a UItemContainerComponent subclass — same grid, replication, and item-object plumbing as inventory/stash. |
| Capture-before-consume | Material influence is read before ingredients are removed; consuming first would leave operations with an empty context. |
| Stateless operations | UCraftingOperation instances carry no state — recipes reference TSubclassOf<> and the container calls the CDO. The FRecipeAffixContext is the only craft-time data passed in. |
| Broken references surface, not hide | Missing/empty modifier resolution logs LogCrafting errors and leaves tokens literal rather than silently degrading. |
Core Concepts¶
Why Crafting Works This Way¶
Crafting follows the GDD's deterministic-influence model: materials are not random-reroll buttons but intent expressed through three escalating tiers of certainty. The cube is the staging area; the recipe is the rule; the affix context is the resolved intent.
The system is designed so new material kinds (Ichor, Lure variants) and new operations slot in without touching the execution core — operations read a single context struct, and the tooltip formatter dispatches by material category so future resolvers register without ViewModel churn.
Material Categories → Influence Tiers¶
ECraftingMaterialCategory maps each material to how it shapes the outcome. Priority is Specific > Family > Bias — a guaranteed specific modifier overrides everything else.
| Category | Influence | Determinism |
|---|---|---|
Currency |
None (drives major operations only) | n/a |
Shard |
Guarantees one specific modifier ID | Full |
Ichor |
Forces a modifier from guaranteed families (weighted roll within) | Family-bounded |
Lure |
Biases family weights (no guarantee) | Soft |
RawMaterial |
Basic component, no affix influence | n/a |
The Affix Context¶
FRecipeAffixContext is the single carrier of material intent through a craft:
| Field | Source category | Meaning |
|---|---|---|
GuaranteedSpecificModifierID |
Shard | Exact modifier; if set, all other influence ignored |
GuaranteedFamilies |
Ichor | Modifier must come from one of these families |
BiasedFamilies |
Lure | These families get weighted up |
AggregatedBiasMultiplier |
Lure | Multiplier applied to biased family weights |
Authoring a Reagent (FCraftingMaterialFragment)¶
An influence reagent is just an item carrying a FCraftingMaterialFragment. Which fields you populate decides the
reagent's tier — the MaterialCategory enum is a label (runtime logic keys off field presence, not the enum value),
so authoring is "fill in exactly one influence channel."
| Field | Sets context field | Category |
|---|---|---|
GuaranteedSpecificModifierID |
GuaranteedSpecificModifierID |
Shard |
GuaranteedModifierFamilies |
GuaranteedFamilies (AddUnique) |
Ichor |
BiasedModifierFamilies |
BiasedFamilies (AddUnique) |
Lure |
BiasMultiplier |
AggregatedBiasMultiplier (*=) |
Lure |
ApplyToAffixContext folds one fragment's fields into the shared FRecipeAffixContext with strict priority:
- Specific returns early. If
GuaranteedSpecificModifierIDis set it writes that ID and returns — family/bias fields on the same fragment are ignored. Exact intent overrides everything. - Families are additive. Each guaranteed family is
AddUnique-merged, so multiple Ichors in the cube union their families rather than overwrite. - Bias multiplies. Biased families are
AddUnique-merged andAggregatedBiasMultiplier *= BiasMultiplier, so two bias reagents on a shared family stack multiplicatively.
Affix Influence & Context Capture¶
GetMaterialInfluence() walks every item currently in the cube, inspects each FCraftingMaterialFragment, and aggregates the result into one FRecipeAffixContext.
Ordering Is Load-Bearing¶
Server_ExecuteCraft (server):
1. Match + validate recipe
2. Context = GetMaterialInfluence() ◄── materials still present
3. ConsumeIngredients(Recipe) ◄── materials removed
4. ApplyOutcome(Outcome, Recipe, Context)
The context is captured at step 2 and passed as a parameter through ApplyOutcome and into each UCraftingOperation::Execute. Earlier code consumed ingredients first and then re-derived the context from the (now empty) cube inside ApplyOutcome, producing a default-constructed context — no specific ID, no families, no bias — which made every shard/ichor/lure recipe silently fail (operations returned false). Capturing once, up front, and threading the value down is the fix and the contract: ApplyOutcome and Execute never call GetMaterialInfluence() themselves.
Recipe Outcomes & Operations¶
ERecipeOutcomeType selects what a successful craft does. Each outcome runs an ordered list of TSubclassOf<UCraftingOperation>, each receiving the captured context.
| Outcome | Behaviour | Operations run against |
|---|---|---|
CreateNew |
Spawn a brand-new item | — (item produced directly) |
ModifyExisting |
Apply operations to the target item in place | The existing target item |
TransformItem |
Replace the input with a new output item | The newly created item |
UCraftingOperation is abstract, blueprintable, and stateless:
| Method | Parameters | Purpose |
|---|---|---|
CanExecute |
(Item, PoolManager) |
Pre-flight check run before resources are consumed |
Execute |
(Item, Context, PoolManager) |
Perform the operation using the affix context |
GetFailureReason |
— | ECraftingValidationResult for UI feedback |
GetDisplayName |
— | Debug/UI label |
Because operations are stateless CDOs, the only craft-specific data they see is the FRecipeAffixContext — which is exactly why its correct, pre-consume value matters.
Fail-Safe Execution (the no-consume contract)¶
A craft that cannot do anything must not consume reagents. Two rules enforce it:
Outcome selection rolls only among outcomes whose operations can actually execute.
SelectRandomOutcomeFromIndices filters to executable outcomes and renormalizes the weights. This restores the
recipe contract — RNG decides which outcome, never if — and closes the free re-click outcome-fishing loop
a blockable roll allowed. The Crafting Manager's Monte Carlo simulation mirrors the same selection so its odds match
the game's. When every executable outcome has zero weight, the pick is uniform among the allowed** indices rather
than falling back to the unrestricted (blockable) roll.
An operation that mutates and then returns false breaks the contract. Execute()'s mutation contract is
documented on the base class: returning false asserts nothing was changed. Concretely — RerollModifiers
snapshots the lane and restores it when the filtered pool turns out empty, and RerollAllAffixes aggregates with
|= so any mutated lane consumes. TransformItem re-adds the original item when the output cannot fit the
freed cube space, rather than destroying it.
Pre-Click Failure Affordances¶
Failure is surfaced before the click, not as an error afterwards. CraftingController::ShouldDimCubeItem
verdicts each cube shard against the target item's lanes; shards that cannot contribute dim to 0.35 opacity (the
same treatment two-handers get). Verdicts are re-run after in-place crafts (RefreshCubeTiles).
Footer affordance labels reserve their space from window open and only animate opacity (0.25s ease-out), so the footer never reflows as verdicts change.
Layout gotcha worth keeping: a
SizeBoxreserve is voided by aCollapsedchild. Use opacity, not visibility, to hide something whose space must stay reserved.
Reagent & Recipe Catalog¶
The three shipped influence reagents, one per determinism tier. The recipe matches on ingredients (target equipment + the reagent); the operation reads the captured context.
| Reagent | Category | Influence | Recipe | Operation |
|---|---|---|---|---|
| Shard of Hemorrhage | Shard | Guarantees a specific Bleed modifier | Recipe_Shard_Of_Hemorrhage |
AddGuaranteedModifierOperation |
| Ichor of Blight | Ichor | Rolls within the Corruption family | Recipe_Ichor_Of_Blight |
AddRandomAffixOperation |
| Lure of Cinders | Lure | Biases the Fire theme | Recipe_Lure_Of_Cinders |
AddRandomAffixOperation |
Naming convention: RecipeName mirrors its reagent — there is no separate craft verb (no Imbue/Anoint/Coax).
The cosmetic recipe name only surfaces beside the reagent at confirm-time; the player-facing mechanics live in
RecipeDescription.
Asset layout:
| Asset kind | Path |
|---|---|
| Reagents | /Game/DataAssets/Items/ItemManifests/Crafting/ |
| Recipes | /Game/DataAssets/Crafting/<Family>/ |
Cost vs Certainty¶
ResourceCost scales with determinism — the more precisely you control the outcome, the dearer the craft:
The exact numbers are tuned in the recipe assets; the ordering is the contract.
Bias Is a Nudge, Not a Guarantee¶
BiasMultiplier up-weights the spawn weight of every modifier whose family is in BiasedFamilies, inside
CreateWeightedListWithBias. In a large pool a single biased family stays a soft increase (the rest of the pool still
competes), which is exactly why Lure is the lowest-determinism tier: the shipped Lure of Cinders leans the roll
toward the Fire theme, so Fire ends up over-represented — not guaranteed. The strength of the nudge is tuned in the
reagent asset.
Material Tooltip Range Resolution¶
Crafting materials author a description containing a {MODIFIER_EFFECT_RANGE} token. FCraftingDescriptionFormatter resolves that token to the referenced modifier's effect line spanning its tier value ranges, so a shard tooltip shows the actual range it would grant.
Material description template (authored)
"Imbues {MODIFIER_EFFECT_RANGE} bleed damage"
│
▼ FCraftingDescriptionFormatter::FormatCraftingMaterialDescription
Pool->FindModifierByID(GuaranteedSpecificModifierID)
│
▼ ComputeTierUnion over Tier1..Tier5 of TiersValueRanges
{ Min, Max }
│
▼ FModifierDefinition::FormatRangeDescription(Min, Max)
"(8-24)" (collapses to single value when Min == Max)
│
▼ FText::Format with FFormatNamedArguments (localization preserved)
"Imbues (8-24) bleed damage"
Resolution Rules¶
- Tier union: the displayed range is the union of
Tier1..Tier5entries inFModifierDefinition::TiersValueRanges(lowest min, highest max across populated tiers). - Min == Max collapse:
FormatRangeDescriptionshows a single value instead of(N-N). - Failure is loud: a null pool, an unknown modifier ID, or a modifier with no populated tier ranges logs a
LogCraftingerror and leaves the{MODIFIER_EFFECT_RANGE}token literal in the tooltip. A broken modifier reference is a content bug for QA to catch, not something to paper over. - Category dispatch: the formatter is stateless and dispatches by material category (specific / family / bias), so future Ichor/Lure tooltip resolvers register without ViewModel changes.
Pool Resolution¶
Both craft execution and tooltip formatting need the active UModifierPoolManager. UModifierSubsystem::GetPoolManagerFromWorld(WorldContext) consolidates the World → GameInstance → subsystem → pool manager lookup chain (with a current-play-world fallback) into one static accessor, so callers don't re-walk that chain. FRolledModifier::GetDefinition resolves its definition through the same path (its cached definition pointer is mutable for lazy resolution).
Public Contracts¶
UCraftingContainerComponent¶
| Method | Parameters | Purpose |
|---|---|---|
CheckCurrentRecipe |
— | Match cube contents → FCraftingMatchResult |
GetCurrentMatchedRecipe |
— | The currently matched recipe, if any |
HasValidRecipe |
— | Whether current contents form a valid recipe |
Server_ExecuteCraft |
— | Server RPC: capture context, consume, apply outcome |
CanExecuteCraft |
— | Validation result for the current craft |
GetMaterialInfluence |
— | Aggregate FRecipeAffixContext from cube materials |
| Delegate | Payload | When Fired |
|---|---|---|
OnRecipeStateChanged |
FCraftingMatchResult |
Recipe match validity changes |
OnCraftingComplete |
FRecipeOutcome, FEquipmentModifiers |
Craft succeeds (owning client) |
OnCraftingFailed |
ECraftingValidationResult |
Craft fails |
FCraftingDescriptionFormatter¶
| Method | Parameters | Purpose |
|---|---|---|
FormatCraftingMaterialDescription |
(Template, Material, Pool) |
Resolve {MODIFIER_EFFECT_RANGE} against the material's modifier |
Data Types¶
| Struct / Enum | Purpose |
|---|---|
ECraftingMaterialCategory |
Currency / Shard / Ichor / RawMaterial / Lure |
ERecipeOutcomeType |
CreateNew / ModifyExisting / TransformItem |
ECraftingValidationResult |
Why a craft can/can't proceed |
FRecipeAffixContext |
Aggregated material intent (specific / families / bias) |
FCraftingMatchResult |
Matched recipe + validation state |
FCraftingModifierEntry / FCraftingResultContext |
Result-panel display data |
Source References¶
| Component | Location |
|---|---|
UCraftingContainerComponent |
Public/Crafting/Components/CraftingContainerComponent.h |
UCraftingOperation |
Public/Crafting/Operations/CraftingOperation.h |
| Crafting types / affix context | Public/Crafting/Types/CraftingTypes.h |
UCraftingSubsystem |
Public/Crafting/Subsystems/CraftingSubsystem.h |
| Recipe data asset | Public/Crafting/Data/CraftingRecipeDataAsset.h |
FCraftingDescriptionFormatter |
Public/UI/Tooltip/CraftingDescriptionFormatter.h |
FCraftingMaterialFragment |
Public/Inventory/Items/Fragments/ItemFragment.h |
UModifierLibrary::FilterByAffixContext |
Public/Inventory/Modifiers/ModifierLibrary.h |
UModifierLibrary::CreateWeightedListWithBias |
Public/Inventory/Modifiers/ModifierLibrary.h |
FModifierDefinition::FormatRangeDescription |
Public/Inventory/Modifiers/ModifierDefinitions.h |
UModifierSubsystem::GetPoolManagerFromWorld |
Public/Inventory/Subsystems/ModifierSubsystem.h |
| Log category | Public/Crafting/CraftingLog.h |
Related Systems¶
- Item System —
UItemObject, manifest, identity + replication - Item Fragments —
FCraftingMaterialFragmentand fragment composition - Inventory System —
UItemContainerComponentgrid that the cube extends - Equipment System — Where
ModifyExistingoutcomes apply, modifier sources - Remnant Item System — Shares
UModifierPoolManager/ modifier-definition pipeline - Data Authoring Pipeline — Recipe JSON-vs-UASSET storage concerns
Recent Changes¶
| Date | Change | Impact |
|---|---|---|
| 2026-07-23 | Fail-safe craft execution + pre-click failure affordances (FSH-409/410, 57b6f705f + 279d17d27): outcome selection rolls only among executable outcomes with renormalized weights (mirrored in the Monte Carlo sim); the Execute() mutation contract — return false ⇒ nothing changed — documented on the base class and enforced (RerollModifiers lane snapshot/restore, RerollAllAffixes |= aggregation, TransformItem re-adds the original when the output cannot fit); shard grey-out verdicts + reserve-stable fading footer labels; W_Crafting adopted into WidgetForge |
Removes free re-click outcome fishing and a class of silent reagent loss; failure is legible before the click rather than after it |
| 2026-06-17 | Documented the author-side FCraftingMaterialFragment (priority/additive/multiplicative ApplyToAffixContext rules) and the shipped reagent/recipe catalog (Shard of Hemorrhage / Ichor of Blight / Lure of Cinders), with cost-vs-certainty ordering and the "bias is a nudge" note |
Authors and designers can see how to define a reagent and how the three tiers map to operations, costs, and asset paths |
| 2026-04-19 | Material tooltips resolve {MODIFIER_EFFECT_RANGE} via FCraftingDescriptionFormatter + FModifierDefinition::FormatRangeDescription; GetPoolManagerFromWorld consolidates pool lookup |
Shard tooltips show the real tier-union value range; broken modifier IDs surface as LogCrafting errors instead of hiding |
| 2026-04-18 | Capture FRecipeAffixContext before consuming ingredients; thread it through ApplyOutcome and UCraftingOperation::Execute |
Fixes every shard/ichor/lure recipe silently failing on a default-constructed (empty) context |
| 2026-04-13 | Initial crafting runtime: cube container, recipe matching, affix-context aggregation, outcome/operation execution | Server-authoritative crafting loop with deterministic material influence |