Configuring Unlocks
Custom unlocks' displayed names, descriptions, images, buttons and their order in the list can be configured. You can even determine your own unlock conditions, change the displayed text and sprites conditionally and stuff like that.
Sorting
Unlocks are first sorted by their SortingOrder
, then by their state (unlocked, purchasable, available and locked), and then by their SortingIndex
. You can ignore the sorting by the state by setting IgnoreStateSorting
to true
.
Here's an example of how this sorting would work:
SortingOrder = -400
:Unlocked
:SortingIndex = -3
;SortingIndex = 1
;SortingIndex = 2
;
Purchasable
:- ...
Available
:- ...
Locked
:- ...
SortingOrder = -3
:- ...
SortingOrder = 0
(vanilla unlocks go here, with sorting index of 0):- ...
SortingOrder = 1
:- ...
SortingOrder = 500
:- ...
The menu might get weird or even crash if not all unlocks on the current SortingOrder
have IgnoreStateSorting
set to the same value. So make sure that all other unlocks have IgnoreStateSorting
set to true
too.
Overrideable methods
UnlockWrapper
// called when the unlock is initialized and integrated into the game
public virtual void SetupUnlock() { }
// called pretty often to determine if it can be unlocked right now
public virtual void UpdateUnlock()
{
if ((Unlock.nowAvailable = !Unlock.unlocked && CanBeUnlocked()) && UnlockCost is 0)
gc.unlocks.DoUnlockForced(Name, Type);
}
// determines whether the unlock can be unlocked right now
public virtual bool CanBeUnlocked() => UnlockCost > -1
&& Unlock.prerequisites.TrueForAll(c => gc.sessionDataBig.unlocks.Exists(u => u.unlockName == c && u.unlocked));
// gets the unlock's raw name, without any rich text, costs and values
public virtual string GetName() => gc.nameDB.GetName(Name, Unlock.unlockNameType);
// gets the unlock's raw description, without any rich text, costs and values
public virtual string GetDescription() => gc.nameDB.GetName(Name, Unlock.unlockDescriptionType);
// gets the unlock's image (displayed in the menus)
public virtual Sprite GetImage() => RogueFramework.ExtraSprites.TryGetValue(Name, out Sprite image) ? image;
You can see for yourself how these methods are implemented in RogueLibs' source code.
DisplayedUnlock
// called when the button is updated. `UpdateUnlock` is called right before this.
public virtual void UpdateButton() => UpdateButton(IsEnabled, UnlockButtonState.Selected, UnlockButtonState.Normal);
protected void UpdateButton(bool isEnabledOrSelected, UnlockButtonState selected, UnlockButtonState normal)
{
Text = GetFancyName();
State = IsUnlocked ? isEnabledOrSelected ? selected : normal
: Unlock.nowAvailable && UnlockCost > -1 ? UnlockButtonState.Purchasable
: UnlockButtonState.Locked;
}
// called when the button is pressed. See other unlocks' implementations for more info
public abstract void OnPushedButton();
// gets the unlock's "fancy" name, with rich text formatting, costs and point values
public virtual string GetFancyName()
{
/* A lot of stuff, see RogueLibs' source code for more information */
}
// gets the unlock's "fancy" description, with rich text formatting, cancellations, prerequisites and recommendations
public virtual string GetFancyDescription()
{
/* A lot of stuff, see RogueLibs' source code for more information */
}
You can see for yourself how these methods are implemented in RogueLibs' source code.
Examples
- Present (sprite variations)
- Random Items Button
- Categories
Let's say, you want to make an item called Present, and it has 3 different sprites.
First of all, you'll need to create an unlock class deriving from ItemUnlock
:
public class PresentUnlock : ItemUnlock
{
}
Now you can override the DisplayedUnlock
's GetImage
method:
public class PresentUnlock : ItemUnlock
{
public override Sprite GetImage()
{
int rnd = new System.Random().Next(3) + 1;
return gc.gameResources.itemDic[$"Present{rnd}"];
}
}
Then just use your custom unlock in the custom item's initialization:
public class Present : CustomItem, IItemUsable
{
[RLSetup]
public static void Setup()
{
RogueLibs.CreateCustomItem<Present>()
.WithName(new CustomNameInfo("Present"))
.WithDescription(new CustomNameInfo("Open it!"))
.WithSprite(Properties.Resources.Present)
.WithUnlock(new PresentUnlock
{
UnlockCost = 5,
CharacterCreationCost = 3,
LoadoutCost = 3
});
}
}
A simple example - a button in the Item Teleporter's menu, that gives you 5 random items.
public class RandomItemsButton : ItemUnlock
{
[RLSetup]
public static void Setup()
{
RogueLibs.CreateCustomUnlock(new RandomItemsButton())
.WithName(new CustomNameInfo("Random Items")
.WithDescription(new CustomNameInfo("Gives you 5 random items"));
}
public RandomItemsButton()
{
SortingOrder = -100; // appear on top of the menu
IsAvailable = false;
IsAvailableInCC = false;
IsAvailableInItemTeleporter = true; // only in Item Teleporter
}
public override void OnPushedButton()
{
System.Random rnd = new System.Random();
for (int i = 0; i < 5; i++)
{
UnlockWrapper item;
do
{
int index = rnd.Next(Menu.Unlocks.Count);
UnlockWrapper item = Menu.Unlocks[index];
}
// make sure that you don't accidentally purchase a locked item
while (!item.IsUnlocked && item != this);
// it'd be way better and safer to spawn the item yourself,
// but for the sake of simplicity, we'll just simulate the button push
item.OnPushedButton();
}
}
}
So, you want to make categories, like in aToM? Here you go!
public class MyCategory : MutatorUnlock
{
[RLSetup]
public static void Setup()
{
MyCategory category = new MyCategory("MyCustomCategory1");
RogueLibs.CreateCustomUnlock(category)
.WithName(new CustomNameInfo("My Custom Category 1"))
.WithDescription(new CustomNameInfo("My Custom Category 1 is very cool and does a lot of great stuff"));
category.SortingOrder = -59;
category.SortingIndex = -1;
int i = 0;
foreach (MutatorUnlock mutator in mutators1)
{
mutator.SortingOrder = -59;
mutator.SortingIndex = i;
category.Contents.Add(mutator);
}
Categories.Add(category);
category = new MyCategory("MyCustomCategory2");
RogueLibs.CreateCustomUnlock(category)
.WithName(new CustomNameInfo("My Custom Category 2"))
.WithDescription(new CustomNameInfo("My Custom Category 2 is really great and accomplishes a ton of cool things"));
category.SortingOrder = -58;
category.SortingIndex = -1;
i = 0;
foreach (MutatorUnlock mutator in mutators2)
{
mutator.SortingOrder = -58;
mutator.SortingIndex = i;
category.Contents.Add(mutator);
}
Categories.Add(category);
RogueLibs.CreateCustomName("CategoryShow", "Interface", new CustomNameInfo("Show"));
RogueLibs.CreateCustomName("CategoryHide", "Interface", new CustomNameInfo("Hide"));
}
public MyCategory(string name) : base(name) { }
public List<UnlockWrapper> Contents = new List<UnlockWrapper>();
public static List<MyCategory> Categories = new List<MyCategory>();
private bool isExpanded;
public bool IsExpanded
{
get => isExpanded;
set
{
if (isExpanded != (isExpanded = value)) // if the value changed
{
// make current category's mutators available/unavailable
foreach (UnlockWrapper mutator in Contents)
mutator.IsAvailable = value;
// if it was expanded, close all other categories
if (value)
foreach (MyCategory category in Categories)
if (category != this) category.IsExpanded = false;
}
}
}
public override string GetFancyName()
{
string name = base.GetFancyName();
name += " - " + gc.nameDB.GetName(IsExpanded ? "CategoryHide" : "CategoryShow", "Interface");
return name;
}
public override void OnPushedButton()
{
if (Menu!.Type == UnlocksMenuType.MutatorMenu)
{
PlaySound("ClickButton");
// toggle the IsExpanded property
IsExpanded = !IsExpanded;
// reopen the menu to update the buttons list
((CustomScrollingMenu)Menu).Menu.OpenScrollingMenu();
}
else base.OnPushedButton();
}
}