Combinable Items
Custom items can be made combinable with other items by implementing the IItemCombinable
interface. You can define what kind of items your item is combinable with, what happens when you combine these items, and what tooltips to display in the combinable item's cell, and when hovering over it.
Making items combinable
Just implement the IItemCombinable
interface in your item's class:
public class MyCombinableItem : CustomItem, IItemCombinable
{
public bool CombineFilter(InvItem other) { /* ... */ }
public bool CombineItems(InvItem other) { /* ... */ }
public CustomTooltip CombineTooltip(InvItem other) { /* ... */ }
public CustomTooltip CombineCursorText(InvItem other) { /* ... */ }
}
Plus, your item's type must be "Combine"
:
public override void SetupDetails()
{
Item.itemType = ItemTypes.Combine;
/* ... */
}
CombineFilter
determines what items will be highlighted, when combining the current item.
CombineItems
combines the current item with the other one. The return value indicates whether it was a success or not. Usually you'd just play a "CantDo"
sound, if the items cannot be combined. Returning true
will also play an animation.
CombineTooltip
determines the tooltip in the upper-left corner of the inventory slot. Text
set to null
will default to an empty string, and Color
set to null
will default to
CombineCursorText
determines the cursor text when hovering over the item. Text
set to null
will default to "Combine", and Color
set to null
will default to
Inventory Slot Preview
Wanna see how your CombineTooltip
will look in the game? Check out this small tool:
Examples
- Centrifuge
- Spice Rack
- Ammo Box
A simple example that inverts the Syringes' effects.
using System.Collections.Generic;
using System.Linq;
namespace RogueLibsCore.Test
{
public class Centrifuge : CustomItem, IItemCombinable
{
[RLSetup]
public static void Setup()
{
/*
RogueLibs.CreateCustomItem<Centrifuge>()
.WithName(new CustomNameInfo("Centrifuge"))
.WithDescription(new CustomNameInfo("Combine with a syringe to invert its effect."))
.WithSprite(Properties.Resources.Centrifuge)
.WithUnlock(new ItemUnlock
{
UnlockCost = 10,
LoadoutCost = 5,
CharacterCreationCost = 3,
Prerequisites = { VanillaItems.Antidote },
});
*/
}
public override void SetupDetails()
{
Item.itemType = ItemTypes.Combine;
Item.itemValue = 8;
Item.initCount = 10;
Item.stackable = true;
Item.hasCharges = true;
}
private static readonly Dictionary<string, string> invertDictionary = new Dictionary<string, string>
{
[VanillaEffects.Poisoned] = VanillaEffects.RegenerateHealth,
[VanillaEffects.Slow] = VanillaEffects.Fast,
[VanillaEffects.Weak] = VanillaEffects.Strength,
[VanillaEffects.Acid] = VanillaEffects.Invincible,
[VanillaEffects.Confused] = VanillaEffects.Invisible,
};
static Centrifuge()
{
foreach (KeyValuePair<string, string> pair in invertDictionary.ToArray())
invertDictionary.Add(pair.Value, pair.Key);
}
public bool CombineFilter(InvItem other) => other.invItemName == VanillaItems.Syringe
&& other.contents.Count > 0 && invertDictionary.ContainsKey(other.contents[0]);
public bool CombineItems(InvItem other)
{
if (!CombineFilter(other)) return false;
other.contents[0] = invertDictionary[other.contents[0]];
Count--;
gc.audioHandler.Play(Owner, VanillaAudio.CombineItem);
return true;
}
public CustomTooltip CombineCursorText(InvItem other) => default;
public CustomTooltip CombineTooltip(InvItem other) => default;
}
}
A simple example, with custom hooks to keep track of seasoned items.
See more about custom hooks here.
using UnityEngine;
namespace RogueLibsCore.Test
{
[ItemCategories(RogueCategories.Food, RogueCategories.Health)]
public class SpiceRack : CustomItem, IItemCombinable
{
[RLSetup]
public static void Setup()
{
RogueLibs.CreateCustomItem<SpiceRack>()
.WithName(new CustomNameInfo("Spice Rack"))
.WithDescription(new CustomNameInfo("Combine with any food item to increase its healing properties."))
.WithSprite(Properties.Resources.SpiceRack)
.WithUnlock(new ItemUnlock
{
UnlockCost = 10,
LoadoutCost = 3,
CharacterCreationCost = 2,
Prerequisites = { VanillaItems.FoodProcessor },
});
SeasonCursorText = RogueLibs.CreateCustomName("SeasonItem", NameTypes.Interface, new CustomNameInfo("Season"));
}
private static CustomName SeasonCursorText = null!;
public override void SetupDetails()
{
Item.itemType = ItemTypes.Combine;
Item.itemValue = 4;
Item.initCount = 10;
Item.rewardCount = 15;
Item.stackable = true;
Item.hasCharges = true;
}
public bool CombineFilter(InvItem other)
{
if (other.itemType != ItemTypes.Food || other.healthChange is 0
|| !other.Categories.Contains(RogueCategories.Food)) return false;
SpicedHook? hook = other.GetHook<SpicedHook>();
return hook is null || hook.Spiciness < 3;
}
public bool CombineItems(InvItem other)
{
if (!CombineFilter(other)) return false;
SpicedHook hook = other.GetHook<SpicedHook>() ?? other.AddHook<SpicedHook>();
hook.IncreaseSpiciness();
Count--;
gc.audioHandler.Play(Owner, VanillaAudio.CombineItem);
return true;
}
public CustomTooltip CombineCursorText(InvItem other) => SeasonCursorText;
public CustomTooltip CombineTooltip(InvItem other)
{
if (!CombineFilter(other)) return default;
SpicedHook? hook = other.GetHook<SpicedHook>();
int bonus = hook is null ? (int)Mathf.Ceil(other.healthChange / 4f) : hook.HealthBonus;
return new CustomTooltip($"+{bonus}", Color.green);
}
private class SpicedHook : HookBase<InvItem>
{
protected override void Initialize()
=> HealthBonus = (int)Mathf.Ceil(Instance.healthChange / 4f);
public int HealthBonus { get; private set; }
public int Spiciness { get; private set; }
public void IncreaseSpiciness()
{
if (Spiciness is 3) return;
Spiciness++;
Instance.healthChange += HealthBonus;
}
}
}
}
A pretty complicated example with a lot of math.
using UnityEngine;
namespace RogueLibsCore.Test
{
[ItemCategories(RogueCategories.Technology, RogueCategories.GunAccessory, RogueCategories.Guns)]
public class AmmoBox : CustomItem, IItemCombinable
{
[RLSetup]
public static void Setup()
{
RogueLibs.CreateCustomItem<AmmoBox>()
.WithName(new CustomNameInfo("Ammo Box"))
.WithDescription(new CustomNameInfo("Combine with any refillable weapon to refill it. Limited ammo."))
.WithSprite(Properties.Resources.AmmoBox)
.WithUnlock(new ItemUnlock
{
UnlockCost = 10,
LoadoutCost = 5,
CharacterCreationCost = 3,
Prerequisites = { VanillaItems.KillAmmunizer },
});
}
public override void SetupDetails()
{
Item.itemType = ItemTypes.Combine;
Item.itemValue = 4;
Item.initCount = 100;
Item.rewardCount = 200;
Item.hasCharges = true;
Item.stackable = true;
}
public bool CombineFilter(InvItem other) => other.itemType == ItemTypes.WeaponProjectile && !other.noRefills;
public bool CombineItems(InvItem other)
{
if (!CombineFilter(other))
{
gc.audioHandler.Play(Owner, VanillaAudio.CantDo);
return false;
}
if (other.invItemCount >= other.maxAmmo)
{
Owner!.SayDialogue("AmmoDispenserFull");
gc.audioHandler.Play(Owner, VanillaAudio.CantDo);
return false;
}
int amountToRefill = other.maxAmmo - other.invItemCount;
float singleCost = (float)other.itemValue / other.maxAmmo;
if (Owner!.oma.superSpecialAbility && Owner.agentName is VanillaAgents.Soldier or VanillaAgents.Doctor)
singleCost = 0f;
int affordableAmount = (int)Mathf.Ceil(Count / singleCost);
int willBeBought = Mathf.Min(affordableAmount, amountToRefill);
int willBeReduced = (int)Mathf.Min(Count, willBeBought * singleCost);
Count -= willBeReduced;
other.invItemCount += willBeBought;
Owner.SayDialogue("AmmoDispenserFilled");
gc.audioHandler.Play(Owner, VanillaAudio.BuyItem);
return true;
}
public CustomTooltip CombineTooltip(InvItem other)
{
if (!CombineFilter(other)) return default;
int amountToRefill = other.maxAmmo - other.invItemCount;
if (amountToRefill == 0) return default;
float singleCost = (float)other.itemValue / other.maxAmmo;
if (Owner!.oma.superSpecialAbility && Owner.agentName is VanillaAgents.Soldier or VanillaAgents.Doctor)
singleCost = 0f;
int cost = (int)Mathf.Floor(amountToRefill * singleCost);
int canAfford = (int)Mathf.Ceil(Count / singleCost);
return "+" + Mathf.Min(amountToRefill, canAfford) + " (" + Mathf.Min(cost, Count) + ")";
}
public CustomTooltip CombineCursorText(InvItem other) => gc.nameDB.GetName("RefillGun", NameTypes.Interface);
// it's one of the vanilla dialogues, so there's no need to define it in the mod
}
}