# Combines

> Combines turn one item dropped onto another into new items: two herbs make a mixed herb, poison and arrows make poison arrows. Write them as recipe rows in a Combine Set, or as a Blueprint class.

Markdown version of https://rigbak.com/docs/universal-player-combat/inventory-combines, a page of the Universal Player Combat documentation. Link that page when citing it.

## Set it up (5 minutes)

1. **Write the recipe**

   Right-click in the Content Browser, **UPC > Items > Combine Set**, name it `CombineSet_MyCrafting` and add a **Recipes** row: **Item A** and **Item B** `Item_UPC_Herb`, **Results** one `Item_UPC_MixedHerb`, **Seconds** 1.

   **Details panel: CombineSet_MyCrafting** (data asset, in Content Browser)

   **Category: Recipes**

   | Property | Value |
   | --- | --- |
   | Recipes | 1 Array element |
   | Recipes > Index [0] > Item A | Item_UPC_Herb |
   | Recipes > Index [0] > Count A | 1 |
   | Recipes > Index [0] > Item B | Item_UPC_Herb |
   | Recipes > Index [0] > Count B | 1 |
   | Recipes > Index [0] > Results | 1 Array element |
   | Recipes > Index [0] > Results > Index [0] > Item | Item_UPC_MixedHerb |
   | Recipes > Index [0] > Results > Index [0] > Count | 1 |
   | Recipes > Index [0] > Order Matters | false |
   | Recipes > Index [0] > Required Tags | no tags |
   | Recipes > Index [0] > Seconds | 1 |

   **Category: Combines**

   | Property | Value |
   | --- | --- |
   | Combines | 0 Array elements |

   *Your own set with one row: two herbs make one mixed herb, a second each.*
2. **List it on a config**

   Duplicate `Demo/Items/Inventory/InventoryConfig_UPC_SurvivalGrid` as `InventoryConfig_MyGrid` and replace `CombineSet_UPC_Demo` in its **Combine Sets** with your set.

   **Details panel: InventoryConfig_MyGrid** (data asset, in Content Browser)

   **Category: Combines**

   | Property | Value |
   | --- | --- |
   | Combine Sets | 1 Array element |
   | Combine Sets > Index [0] | CombineSet_MyCrafting |

   **Category: Timing**

   | Property | Value |
   | --- | --- |
   | Combine At Once | false |

   *Your copy of the survival grid config, with your set in place of the demo's. Combine At Once stays off, so each combine waits its Seconds behind a progress bar.*
3. **Give the demo player the config and two herbs**

   On `BP_UPC_Character`'s **InventoryComponent**, set **Config** to `InventoryConfig_MyGrid` and add `Item_UPC_Herb` to **Starting Items** twice (herbs do not stack, so each lies in its own cell).
4. **Open your config on the grid screen**

   In `BPC_UPC_InventoryScreens` (`Demo/UI/Widgets/Inventory`, on `BP_UPC_PlayerController`), add an **Inventory Screen Classes** row in Class Defaults: `InventoryConfig_MyGrid` with `WBP_UPC_InventoryScreen_SurvivalGrid`. Without a row the open world list opens, which pauses the game, so a timed combine cannot land ([Inventory Screens](https://rigbak.com/docs/universal-player-combat/inventory-screens#copy-a-screen-into-your-game)).
5. **Drop one herb on the other**

   Press Play in `LVL_UPC_Demo`, press **I** and drag one herb onto the other. A progress bar fills on its tile and a second later a mixed herb takes their place. **Combine** in one herb's actions, then the other herb, does the same.

The demo character has what combines need. Yours needs a UPC Equipment Manager (without it nothing combines) and a call to **Notify Ability System Ready** (without it a combine finishes at once, with no progress bar; [Character Setup](https://rigbak.com/docs/universal-player-combat/character-setup)). A combine runs inside one inventory, so a drop between the inventory and a chest never combines.

---

## Recipes: the rows you fill

| Setting | Does |
| --- | --- |
| **Item A**, **Count A** | First item and the units one combine uses. |
| **Item B**, **Count B** | Second item and units; may equal Item A (two herbs). |
| **Results** | What one combine makes, each an item and count (an empty bottle can come back). The first result goes where the config's **Combine Result Place** says (default: onto the item it was combined onto). When that combine leaves a stack behind or the result does not fit, and for every other result, it lands as a pickup does: same-item stacks first, then the first free place in container order, else past the end of the first carried container that takes new items. A result goes on the belt only into a slot its ingredient emptied, if the Filter allows. |
| **Order Matters** | On: only Item A dropped onto Item B combines. Off (default): either way round. |
| **Required Tags** | Tags the character must hold (a learned skill, a workbench zone), added and removed by your code (a trigger zone). Without them the drop is refused with `UPC.Inventory.Refused.Locked`. Empty needs none. |
| **Seconds** | Time per combine behind a progress bar; moving stops it. 0 (default), or a config with **Combine At Once**, makes it instant. A combine of your own sets time with **Unit Timing**. |

A drop runs the row as many times as the dragged units and other stack allow. The demo's Dip Arrows turns one poison and five arrows into five poison arrows, so twelve arrows dragged onto three flasks run it twice: ten poison arrows, two arrows and a flask left. A row can require a workbench and return the bottle:

**Details panel: CombineSet_MyWorkbench** (data asset, in Content Browser)

**Category: Recipes**

| Property | Value |
| --- | --- |
| Recipes | 1 Array element |
| Recipes > Index [0] > Item A | Item_MyHerb |
| Recipes > Index [0] > Count A | 2 |
| Recipes > Index [0] > Item B | Item_MyBottle |
| Recipes > Index [0] > Count B | 1 |
| Recipes > Index [0] > Results | 2 Array elements |
| Recipes > Index [0] > Results > Index [0] > Item | Item_MyPotion |
| Recipes > Index [0] > Results > Index [0] > Count | 1 |
| Recipes > Index [0] > Results > Index [1] > Item | Item_MyEmptyBottle |
| Recipes > Index [0] > Results > Index [1] > Count | 1 |
| Recipes > Index [0] > Order Matters | false |
| Recipes > Index [0] > Required Tags | MyGame.State.AtWorkbench |
| Recipes > Index [0] > Seconds | 2 |

**Category: Combines**

| Property | Value |
| --- | --- |
| Combines | 0 Array elements |

*A set with one row: two herbs and a bottle make a potion at a workbench, and the empty bottle comes back. The row is turned down with Locked until the character holds the tag.*

---

## A combine of your own

For what a row cannot say (rounds counted into a magazine), make a Blueprint child of **UPC Inventory Combine** and list it in a set's **Combines**. Characters share one copy, so keep no state in it. The demo's Load Holder and Coat Weapon are two to copy ([Inventory Rules and Combines in Blueprint](https://rigbak.com/docs/universal-player-combat/inventory-blueprint#combines)).

| Function | Answers |
| --- | --- |
| **Matches** | Whether it handles **Item** dropped onto **Target**, from the two items. |
| **Get Units** | Units the drop makes now, 0 to the context's **Count**; 0 refuses, **Out Reason** a tag under `UPC.Inventory.Refused`. Default: every unit asked. |
| **Apply Unit** | Makes one unit, on the server. |
| **Unit Timing** | Optional: seconds per unit, sounds, and whether moving stops it (default half a second, stopped by moving). |

Matches gets the two items. Get Units and Apply Unit get a **Context**: the **Inventory**, the dropped **Item**, the **Target** it landed on (not a node's Target pin) and **Count**, the units still asked for. One Apply Unit is one unit, here one round loaded. Ten rounds dropped on a magazine holding 25 of 30: Get Units gets Count 10 and answers 5 (the room left), so five units run, asking again before each with Count 5, then 4, down to 1.

### A magazine, step by step

These steps build on the quick start's `InventoryConfig_MyGrid` and `BP_UPC_Character`.

1. Make `Item_MyRound` (**Max Stack Size** 60), `Item_MyMagazine` (Max Stack Size 1, so each magazine is its own item) and a gameplay tag `MyGame.Ammo.Loaded`. Each magazine counts its rounds in that tag as a [stat tag](https://rigbak.com/docs/universal-player-combat/inventory#per-item-state-stat-tags), a count kept on one owned item.
2. Make `BP_MagazineCombine` with **Instance Editable** variables `Round` and `Magazine` (UPC Item Definition) and `Capacity` (Integer, 30), and override the three functions:

Blueprint graph, nodes in reading order:

- **Comment box: BP_MagazineCombine · Matches**

  - **Matches** (event); outputs: Item, Target
  - **Get Item Def** (pure function); inputs: Target; outputs: Return Value
  - **Round** (variable)
  - **==** (operator); inputs: A, B; outputs: Return Value
  - **Get Item Def** (pure function); inputs: Target; outputs: Return Value
  - **Magazine** (variable)
  - **==** (operator); inputs: A, B; outputs: Return Value
  - **AND** (operator); inputs: A, B; outputs: Return Value
  - **Return Node** (function); inputs: Return Value

Blueprint graph, nodes in reading order:

- **Comment box: BP_MagazineCombine · Get Units: the least of the rounds dragged, the rounds in the stack and the room left**

  - **Get Units** (event); outputs: Context
  - **Break UPC Inventory Combine Context** (break struct); inputs: UPC Inventory Combine Context; outputs: Inventory, Item, Target, Count
  - **Get Item Stack Count** (pure function); inputs: Target, Instance; outputs: Return Value
  - **MIN** (operator); inputs: A, B; outputs: Return Value
  - **Capacity** (variable)
  - **Get Stat Tag Stack Count** (pure function); inputs: Target, Tag = MyGame.Ammo.Loaded; outputs: Return Value
  - **-** (operator); inputs: A, B; outputs: Return Value
  - **MIN** (operator); inputs: A, B; outputs: Return Value
  - **Return Node** (function); inputs: Out Reason = UPC.Inventory.Refused.Full, Return Value

Blueprint graph, nodes in reading order:

- **Comment box: BP_MagazineCombine · Apply Unit: one round into the magazine**

  - **Apply Unit** (event); outputs: Context
  - **Break UPC Inventory Combine Context** (break struct); inputs: UPC Inventory Combine Context; outputs: Inventory, Item, Target, Count
  - **Add Stat Tag Stack** (function; Target is UPC Inventory Item Instance); inputs: Target, Tag = MyGame.Ammo.Loaded, Stack Count = 1
  - **Consume Item Instance** (function; Target is UPC Inventory); inputs: Target, Instance, Count = 1; outputs: Return Value

3. Make `CombineSet_MyShooter`, add your combine to its **Combines**, and pick your two items:

**Details panel: CombineSet_MyShooter** (data asset, in Content Browser)

**Category: Recipes**

| Property | Value |
| --- | --- |
| Recipes | 0 Array elements |

**Category: Combines**

| Property | Value |
| --- | --- |
| Combines | 1 Array element |
| Combines > Index [0] | BP_MagazineCombine |
| Combines > Index [0] > Round | Item_MyRound |
| Combines > Index [0] > Magazine | Item_MyMagazine |
| Combines > Index [0] > Capacity | 30 |
| Combines > Index [0] > Unit Timing > Seconds | 0.5 |
| Combines > Index [0] > Unit Timing > Cancel on Move | true |

*Your own set with the magazine combine listed. Each entry carries its own Unit Timing: half a second per round, stopped by moving, unless you change it here.*

4. Add `CombineSet_MyShooter` to `InventoryConfig_MyGrid`'s **Combine Sets**, below your crafting set. In `BP_UPC_Character`'s **Starting Items**, add one `Item_MyMagazine` and ten `Item_MyRound` entries (they join into one stack of 10). Drag the rounds onto the magazine on the grid screen to load it, one round every half second.

<details>
<summary>Test it without a screen (Advanced)</summary>

Give `BP_UPC_Character` two graphs: B sends the Request Move a screen's drag sends, and the load's end prints the rounds loaded:

Blueprint graph, nodes in reading order:

- **Comment box: BP_UPC_Character · B drops the rounds on the magazine**

  - **B** (event); outputs: Pressed, Released, Key
  - **Inventory Component** (variable)
  - **Find First Item by Definition** (pure function); inputs: Target, Item Def = Item_MyRound; outputs: Return Value
  - **Find First Item by Definition** (pure function); inputs: Target, Item Def = Item_MyMagazine; outputs: Return Value
  - **Get Item Placement** (pure function); inputs: Target, Item; outputs: Out Placement, Return Value
  - **Request Move** (function; Target is UPC Inventory); inputs: Target, Item, Count = 10, Destination

Blueprint graph, nodes in reading order:

- **Comment box: BP_UPC_Character · print the load when it ends**

  - **On Equip Sequence Ended (EquipmentManagerComponent)** (event); outputs: Cancelled, Reason
  - **Inventory Component** (variable)
  - **Find First Item by Definition** (pure function); inputs: Target, Item Def = Item_MyMagazine; outputs: Return Value
  - **Get Stat Tag Stack Count** (pure function); inputs: Target, Tag = MyGame.Ammo.Loaded; outputs: Return Value
  - **Print String** (function); inputs: In String

Press Play, then **B**. Five seconds later the print shows 10; walk off mid-load and it shows the rounds loaded so far.

</details>

---

## Combine from a button, and show it

| Node | Does |
| --- | --- |
| **Request Combine** | Combines the whole stack of **Item** with **Target**, an item wherever it is (poison reaches the sword in hand). |
| **Can Combine** (UPC Inventory Statics) | Request Combine's check: **Out Results** is what one combine of a recipe makes, **Out Reason** why not. |
| **On Combined** (inventory) | As units land: the two items, the recipe or combine, and **Units** landed together (1 per unit of a timed combine, the full count when all land at once). |
| **On Equip Step Started** (equipment manager) | As each unit starts, with its seconds, for a progress bar. |
| **On Equip Sequence Ended** | The load finished or stopped, with a reason. |

An item's Details panel lists recipes naming it under **Combines With** ("Herb + Herb -> Mixed Herb"). The screens' Combine mode is on [Inventory Screens](https://rigbak.com/docs/universal-player-combat/inventory-screens#combine).

---

## Debug it

`Log LogUPCInventory Verbose` names each request the server turned down.

<details>
<summary>How a combine runs (Advanced)</summary>

- A drop or **Request Combine** asks the config's Combine Sets in order, each set's recipes before its combines. The first match decides, ahead of going into a bag, joining a stack or trading places.
- **Can Place** answers Combine with the units allowed, or Refused with the reason, and **Request Move** runs the combine as one equipment change, a unit at a time.
- Before each unit the inventory asks again: Get Units for a combine, the stacks and Required Tags for a recipe. A 0 stops the rest with its reason, so leaving a workbench mid-load stops it with `Locked`. Units already made stay, whatever stops the load.
- A load also stops before its next unit when the player moves (Unit Timing's **Cancel on Move**), a screen calls the equipment manager's **Request Cancel Sequence** with either item, either item leaves the inventory, the character gains one of the equipment action's **Cancel Tags** or `UPC.State.EquipmentBlocked`, or another animation cuts it off. **On Equip Sequence Ended** gives a reason under `UPC.Equipment.Cancelled`, or the combine's own.
- Every unit lands together, with no step events and no unit sounds, when the unit time is 0, the config's **Combine At Once** is on ([Containers and Rules](https://rigbak.com/docs/universal-player-combat/inventory-containers#combine-sets-and-timing)), or the character has no equipment action. **On Combined** then fires once, with the full count.
- **Get Playing Step** answers how far the current unit has got, 0 to 1, and how many have landed, for a widget opened mid-load.
- On Equip Step Started's step carries the target as **Source Item**, the dropped item as **Used Item**, and the unit's seconds as **Timing**, counted from **Start Time**.
- One equipment change runs at a time, so the belt refuses a press during a load, and **Block Actions While Running** on the equipment action shuts the action graph for the load. A unit Get Units refuses as it lands has already played its commit sound.
- The inventory judges reach and Access Tags when the player asks, not before each unit.
- A result lands in stacks within its Max Stack Size. A row with several results judges each against the inventory as it stands.
- **Find Combine Recipe** returns a set's row for two items, and **Get Recipes For Item** every row of a config's sets that uses or makes an item, for a tooltip or a recipe book.
- A drop asks about the item under the dropped cell, whatever the dragged item's size, so a wide box of rounds still loads a magazine at a grid's edge.
- Saving a set warns in the Message Log about a row or a result naming no item, a row making nothing, and two rows taking the same pair (the first wins). Saving a config warns about a result no container taking new items lets in.

</details>

<details>
<summary>More combines to write (Advanced)</summary>

- **Magazines by caliber.** List one combine per caliber, or have **Matches** compare a caliber tag both items carry in their **Slot Tags**.
- **A key opening a locked box.** Get Units answers 1 while the key and the box share a lock tag in their Slot Tags (`MyGame.Lock.Crypt`) and the box lacks a `MyGame.Box.Unlocked` stat tag, else 0 with `UPC.Inventory.Refused.NotAdmitted`. Apply Unit adds that stat tag and consumes the key.

</details>

<details>
<summary>The demo</summary>

**Details panel: CombineSet_UPC_Demo** (data asset, in Content Browser)

**Category: Recipes**

| Property | Value |
| --- | --- |
| Recipes | 2 Array elements |
| Recipes > Index [0] > Item A | Item_UPC_Herb |
| Recipes > Index [0] > Count A | 1 |
| Recipes > Index [0] > Item B | Item_UPC_Herb |
| Recipes > Index [0] > Count B | 1 |
| Recipes > Index [0] > Results | 1 Array element |
| Recipes > Index [0] > Results > Index [0] > Item | Item_UPC_MixedHerb |
| Recipes > Index [0] > Results > Index [0] > Count | 1 |
| Recipes > Index [0] > Order Matters | false |
| Recipes > Index [0] > Required Tags | no tags |
| Recipes > Index [0] > Seconds | 1 |
| Recipes > Index [1] > Item A | Item_UPC_Poison |
| Recipes > Index [1] > Count A | 1 |
| Recipes > Index [1] > Item B | Item_UPC_Arrow |
| Recipes > Index [1] > Count B | 5 |
| Recipes > Index [1] > Results | 1 Array element |
| Recipes > Index [1] > Results > Index [0] > Item | Item_UPC_PoisonArrow |
| Recipes > Index [1] > Results > Index [0] > Count | 5 |
| Recipes > Index [1] > Order Matters | false |
| Recipes > Index [1] > Required Tags | no tags |
| Recipes > Index [1] > Seconds | 1 |

**Category: Combines**

| Property | Value |
| --- | --- |
| Combines | 2 Array elements |
| Combines > Index [0] | BP_UPC_Combine_LoadHolder |
| Combines > Index [0] > Unit Timing > Seconds | 0.3 (folded until clicked) |
| Combines > Index [0] > Unit Timing > Start Sound | None (folded until clicked) |
| Combines > Index [0] > Unit Timing > Commit Sound | None (folded until clicked) |
| Combines > Index [0] > Unit Timing > Cancel on Move | true (folded until clicked) |
| Combines > Index [1] | BP_UPC_Combine_CoatWeapon |
| Combines > Index [1] > Poison | Item_UPC_Poison |
| Combines > Index [1] > Weapon Tags | UPC.Slot.Weapon |
| Combines > Index [1] > Poisoned Tag | UPC.Inventory.Stat.Poisoned |
| Combines > Index [1] > Strikes | 3 |
| Combines > Index [1] > Unit Timing > Seconds | 1.5 (folded until clicked) |
| Combines > Index [1] > Unit Timing > Start Sound | None (folded until clicked) |
| Combines > Index [1] > Unit Timing > Commit Sound | None (folded until clicked) |
| Combines > Index [1] > Unit Timing > Cancel on Move | true (folded until clicked) |

*The set all three demo styles list. Mix Herbs and Dip Arrows are plain rows; Load Holder and Coat Weapon are Blueprint combines.*

The party RPG and open world list combine at once; the survival grid shows a progress bar.

</details>

<details>
<summary>What runs where (Multiplayer)</summary>

- The server makes every unit; the owning player's machine asks the same sets, so its **Can Place** and **Can Combine** match the server early. It plays its copy of the load from the server's step about half a round trip later and stops with the server's reason.
- **On Combined** fires on the server and the owning player's machine, once per landing.

</details>

<details>
<summary>C++</summary>

The magazine as a C++ subclass:

```cpp
UCLASS()
class UMyMagazineCombine : public UUPCInventoryCombine
{
	GENERATED_BODY()

public:
	UPROPERTY(EditAnywhere, Category = "Magazine")
	TObjectPtr<UUPCItemDefinition> Round;

	UPROPERTY(EditAnywhere, Category = "Magazine")
	TObjectPtr<UUPCItemDefinition> Magazine;

	UPROPERTY(EditAnywhere, Category = "Magazine")
	int32 Capacity = 30;

	virtual bool Matches_Implementation(const UUPCInventoryItemInstance* Item, const UUPCInventoryItemInstance* Target) const override
	{
		return Item->GetItemDef() == Round && Target->GetItemDef() == Magazine;
	}

	virtual int32 GetUnits_Implementation(const FUPCInventoryCombineContext& Context, FGameplayTag& OutReason) const override
	{
		const int32 Room = Capacity - Context.Target->GetStatTagStackCount(TAG_MyGame_Ammo_Loaded);
		const int32 Units = FMath::Min3(Context.Count, Context.Inventory->GetItemStackCount(Context.Item), Room);
		if (Units < 1)
		{
			OutReason = TAG_UPC_Inventory_Refused_Full;
		}
		return Units;
	}

	virtual void ApplyUnit_Implementation(const FUPCInventoryCombineContext& Context) const override
	{
		Context.Target->AddStatTagStack(TAG_MyGame_Ammo_Loaded, 1);
		Context.Inventory->ConsumeItemInstance(Context.Item, 1);
	}
};
```

- `Inventory/UPCInventoryTypes.h` declares the refusal tags; `TAG_MyGame_Ammo_Loaded` is your own native tag.
- `UUPCInventoryConfig::CombineSets` lists `UUPCCombineSet` assets, each holding `FUPCCombineRecipe` rows in `Recipes` and instanced `UUPCInventoryCombine` objects in `Combines`. `UUPCInventoryConfig::FindCombine` returns the first match as an `FUPCCombineMatch`.
- The load is one `FUPCEquipStep` with `Repeats` set to the units, `SourceItem` the target and `UsedItem` the dropped item.
- `UUPCInventoryComponent::CanCombine` fills the `FUPCCombineMatch`; `OnCombined` carries an `FUPCCombineOutcome`.

</details>

<details>
<summary>Troubleshooting (Advanced)</summary>

- **A drop merges or swaps instead.** Nothing in the config's Combine Sets takes the pair, or the character uses another config.
- **Nothing happens.** Another equipment change is running, or the pawn has no UPC Equipment Manager (a warning under `LogUPCInventory`).
- **A drop is refused.** Print **Out Reason** from **Can Combine** or **Can Place**: `NoCombine`, `NotEnough` (a stack holds too few for one combine), `Locked` (Required Tags missing), or a reason your combine sets, such as `Full`.
- **A load stops at once.** Get Units answered 0 for the next unit, or the player moved during a combine that stops on a move.

</details>

---

Previous page: [Bags and Banks](https://rigbak.com/docs/universal-player-combat/inventory-bags). Next page: [Loot Chests](https://rigbak.com/docs/universal-player-combat/loot-chests)
