Mega Tree API
Node definition
Every field of UpgradeDefinition: what it does, what it defaults to, and what it costs you to get wrong.
One UpgradeDefinition is one line in the tree - a
column of chained level nodes, bought from the top down. A three-level definition draws three
circles, not one.
Identity
| Field | Type | Default | Notes |
|---|---|---|---|
Id required | string | - | Unique, prefix.name, lower case. It is the key in the save file, so never change it after release - a renamed id reads as a different upgrade and the player loses the levels. Rules |
DisplayName | string | - | Name on the node. Required unless you set NameKey. |
NameKey | string | auto | Translation key for the name. Leave it out and Mega Tree makes one for you: <id>.name, filled with your DisplayName in English. Translations |
SortOrder | int | 0 |
Column order inside your sub-tab, left to right. Ties break by id, so the order is stable across launches even if you never set it. |
LegacyIds | string[] | null |
Ids this node used to have. A level saved under one of them moves to the current Id, so renaming does not cost your players their progress. Details |
Levels and price
| Field | Type | Default | Notes |
|---|---|---|---|
MaxLevel | int | 1 |
1 to 20. Each level is its own node, chained to the one before it. |
BaseCost | float | 1000 |
Price of level 1. |
CostMultiplier | float | 1.8 |
Each level costs this much more than the last. |
CostForLevel | Func<int,int> | null |
Full control. Given a 1-based level, return its cost. Overrides the two fields above. |
SellBackRate | float? | null (50%) |
How much of a level's price comes back on a sell, 0 to 1. Affects only this node; clamped, with a log warning if out of range. |
Near 1.0 your upgrade becomes a piggy bank. The player buys to park money and sells when they need it, at no cost. It does not create money out of nothing, but it takes the pressure out of the game's economy.
And the player has no way to know. Two nodes side by side in the same menu, one refunding 90% and one refunding 50%, with nothing on screen explaining the difference. The hover card shows the amount, never the rate.
Use it when your line genuinely calls for it - a very expensive one-off, or a cheap toggle meant to be tried. Not as a default.
The built-in curve is BaseCost * CostMultiplier^(level-1), rounded.
With 1500 and 1.8 that is:
| Level | 1 | 2 | 3 | 4 | 5 |
|---|---|---|---|---|---|
| Cost | 1 500 | 2 700 | 4 860 | 8 748 | 15 746 |
| Running total | 1 500 | 4 200 | 9 060 | 17 808 | 33 554 |
Use the generator to see this table for your own numbers before you ship them.
The player starts with $400. A store expansion runs $600 at level 1 and $13,800 at level 17, about $80,000 for the whole thing - the largest single sink in the game. A licence is $100 to $2,800.
Mega Tree's own lines sit between those: the You tab starts at $250 a level, and Full Day Shift - which changes how the whole store operates - costs $60,000. Price against that, not against a number that feels right.
A flat table instead of a curve
var prices = new[] { 800, 2000, 6000 };
new UpgradeDefinition
{
Id = "bigboxes.capacity",
MaxLevel = 3,
CostForLevel = lvl => prices[lvl - 1], // lvl is 1-based
// ...
}By default the player gets 50% of what a level cost when they sell it back, so a level priced at 1000 refunds 500. That is what every built-in Mega Tree upgrade uses.
You can set SellBackRate on your node, but read the field
description above before you do - a rate far from 50% has consequences that are not obvious.
Presentation
| Field | Type | Default | Notes |
|---|---|---|---|
Icon | UpgradeIcon | null |
A built-in icon by name, or your own PNG. Null falls back to a generic node icon. Icons |
DescribeLevel required | Func<int,string> | - | Given a 1-based level, return the one-line effect on the hover card. Keep it short - the card is about 25 characters wide before the font shrinks. |
ComingSoon | bool | false |
Draws the node dimmed and unbuyable, with "Soon" on the card. For advertising work in progress. |
Nothing checks that
your text matches your effect. If DescribeLevel says "25% faster" and
your patch applies 15%, the player reads 25% and gets 15%, and it will be reported as a Mega Tree
bug. Derive both from the same constant.
const float PerLevel = 0.05f; // one source of truth
DescribeLevel = lvl => $"{lvl * PerLevel * 100:0}% off supply costs",
// ...and in the patch:
amount *= 1f - PerLevel * MegaTreeApi.GetLevel(Id);Gating
| Field | Type | Default | Notes |
|---|---|---|---|
IsUnlocked | Func<bool> | null (always unlocked) |
While it returns false the node is grey and unbuyable. Polled about 3x a second per visible node and cached for 0.25s. Requirements |
LockedReason | Func<string> | null |
The line shown on the card while locked. Without it the card shows a bare "Locked", which reads as a bug. |
RequiredStoreLevel | Func<int,int> | null (no requirement) |
Given a 1-based level, the store level needed to buy it. Return 0 for none. Same shape as CostForLevel, so a line can rise with the shop. |
Leave RequiredStoreLevel null and your node has no level requirement at all. That
is the default, and it is what every mod written before the field existed keeps doing.
// One step per level
RequiredStoreLevel = lvl => lvl * 10,
// Or an explicit table, which is easier to balance and to read back
RequiredStoreLevel = lvl => lvl switch { 1 => 5, 2 => 12, _ => 20 },
The number is the game's own player level - the same one that gates hiring and store growth.
Mega Tree reads it from ExperienceManager.
The player can switch level gating off completely in
BepInEx/config/MegaTree.cfg. When they do, your requirement is ignored
along with everyone else's - you do not need to handle that case.
Callbacks
| Field | Type | Fires |
|---|---|---|
OnLevelChanged | Action<int> |
Right after this node is bought or sold back, with the new level. |
OnApply | Action |
About twice a second, after Mega Tree applies its own upgrades. Not per frame. Events |
A definition using everything
const float PerLevel = 0.05f;
tree.Register(new UpgradeDefinition
{
Id = "bigboxes.discount",
DisplayName = "Bulk Discount",
NameKey = "bigboxes.discount.name",
MaxLevel = 4,
CostForLevel = lvl => 1000 * lvl * lvl,
SellBackRate = 0.5f,
SortOrder = 10,
Icon = UpgradeIcon.Builtin("truck"),
DescribeLevel = lvl => $"{lvl * PerLevel * 100:0}% off supply costs",
IsUnlocked = () => MegaTreeApi.GetLevel("bigboxes.capacity") >= 2,
LockedReason = () => "Needs Bigger Boxes II",
RequiredStoreLevel = lvl => lvl switch { 1 => 5, 2 => 12, _ => 20 },
OnLevelChanged = lvl => Plugin.Log.LogInfo($"discount now {lvl}"),
});