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

FieldTypeDefaultNotes
Id requiredstring- 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
DisplayNamestring- Name on the node. Required unless you set NameKey.
NameKeystringauto Translation key for the name. Leave it out and Mega Tree makes one for you: <id>.name, filled with your DisplayName in English. Translations
SortOrderint0 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.
LegacyIdsstring[]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

FieldTypeDefaultNotes
MaxLevelint1 1 to 20. Each level is its own node, chained to the one before it.
BaseCostfloat1000 Price of level 1.
CostMultiplierfloat1.8 Each level costs this much more than the last.
CostForLevelFunc<int,int>null Full control. Given a 1-based level, return its cost. Overrides the two fields above.
SellBackRatefloat?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.
Two things a rate far from 50% does

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:

Level12345
Cost1 5002 7004 8608 74815 746
Running total1 5004 2009 06017 80833 554

Use the generator to see this table for your own numbers before you ship them.

What the game itself charges, for scale

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

csharp
var prices = new[] { 800, 2000, 6000 };

new UpgradeDefinition
{
    Id           = "bigboxes.capacity",
    MaxLevel     = 3,
    CostForLevel = lvl => prices[lvl - 1],   // lvl is 1-based
    // ...
}
Sell-back is 50% unless you say otherwise

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

FieldTypeDefaultNotes
IconUpgradeIconnull A built-in icon by name, or your own PNG. Null falls back to a generic node icon. Icons
DescribeLevel requiredFunc<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.
ComingSoonboolfalse Draws the node dimmed and unbuyable, with "Soon" on the card. For advertising work in progress.
DescribeLevel is a promise, not a calculation

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.

csharp
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

FieldTypeDefaultNotes
IsUnlockedFunc<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
LockedReasonFunc<string>null The line shown on the card while locked. Without it the card shows a bare "Locked", which reads as a bug.
RequiredStoreLevelFunc<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

FieldTypeFires
OnLevelChangedAction<int> Right after this node is bought or sold back, with the new level.
OnApplyAction About twice a second, after Mega Tree applies its own upgrades. Not per frame. Events

A definition using everything

csharp
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}"),
});