Tech 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 Tech 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 1.3.1 | 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. 1.8 is what every built-in upgrade uses. |
CostForLevel | Func<int,int> | null |
Full control. Given a 1-based level, return its cost. Overrides the two fields above. |
SellBackRate 1.3.1 | float? | null (60%) |
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 60%, 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.
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 60% of what a level cost when they sell it back, so a level priced at 1000 refunds 600. That is what the ten built-in upgrades use.
Since 1.3.1 you can set SellBackRate on your node, but read the
field description below before you do - a rate far from 60% 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 Tech 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 * TechTreeApi.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 just shows nothing, which reads as a bug. |
RequiredStoreLevel | Func<int,int> | null (no requirement) |
1.5.0+ — 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 behaves exactly as it did before
1.5.0 — no level requirement at all. Nothing about an existing mod changes when players
update.
// 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 },
Pick numbers that people will actually reach. The game's own last restocker unlocks at store level 50, and Tech Tree's own upgrades top out around there. A node asking for 80 is a node almost nobody will ever see.
The player can switch level gating off completely in Tech Tree's config. 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 every two seconds, after Tech 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,
SortOrder = 10,
Icon = UpgradeIcon.Builtin("truck"),
DescribeLevel = lvl => $"{lvl * PerLevel * 100:0}% off supply costs",
IsUnlocked = () => TechTreeApi.GetLevel("bigboxes.capacity") >= 2,
LockedReason = () => "Needs Bigger Boxes II",
OnLevelChanged = lvl => Log.LogInfo($"discount now {lvl}"),
});