Skip to main content

Define a Quality Chain

A quality tier is one file at data/<namespace>/mxt/quality/<path>.json, in the mxt:quality registry. A tier says three things: how it is drawn (color and friends); which tier sits above it (next); what the step up costs and requires (upgrade_costs / upgrade_condition); and which ladder it belongs to (quality). What it is called is not up to the definition: the name and description are always derived from the registered id, with the keys quality.mxt.<definition namespace>.<path> (the description adding .description), so translate those two keys in your language file to give the tier words.

The order of the tiers is written nowhere else. No table holds the ladder on its own: the runtime walks next once and gets a straight line.

FieldDefaultWhat it does
colornoneTints the name, written as #RRGGBB (an integer or an [r,g,b] float array is accepted too); always treated as opaque.
value_multiplier1Multiplies the item's currency value.
forging_modifier1Divides the extra steps forging reads.
alchemy_modifier1Divides the brewing duration.
conditionmxt:alwaysWhether an item of this tier can be used.
nextnoneThe id of the tier above; the top tier leaves it out.
upgrade_costs[]What the step up costs.
upgrade_conditionmxt:alwaysWhat the step up requires.
qualitynoneThe name of the ladder, written on the entry tier.

Every field is optional. The three modifiers share one shape, {"description": …, "modifier": …}: modifier is a number or a formula string and defaults to 1, and description gets its own line in the item tooltip only when you write one — leaving it out does not stop the modifier from working. A modifier that is not a finite positive number is treated as 1.

condition is the same gate a binding uses: while it fails, the item cannot be used. The full field list is in Quality.

What You Are Building​

FilePurpose
data/example/mxt/quality/common.jsonThe lowest tier, which is the entry of this ladder; the ladder's name goes here.
data/example/mxt/quality/refined.jsonThe middle tier; the price and the modifier of the step up from common go here.
data/example/mxt/quality/flawless.jsonThe top tier, with no next.

The price is paid in example:qi, which already has a resource/qi.json in Define Aura and Realms; the item carrying the tiers is the KubeJS-registered kubejs:qi_pill from Create Items and Bind Actions with KubeJS.

Step 1 — Three Tiers​

// data/example/mxt/quality/common.json
{
"quality": "example:pill",
"next": "example:refined"
}
// data/example/mxt/quality/refined.json
{
"next": "example:flawless",
"upgrade_costs": [{"id": "example:qi", "amount": 20}],
"value_multiplier": {
"description": "mxt.quality.example.refined.value",
"modifier": 1.25
}
}
// data/example/mxt/quality/flawless.json
{}
  • next points at the tier above, so walking next upwards reads common → refined → flawless.
  • flawless is an empty object: the top tier leaves next out and takes every modifier default. Its name and description are generated from the id like any other tier (mxt.quality.example.flawless), since the definition has neither field in the first place.
  • {"id": …, "amount": …} inside upgrade_costs is the shorthand for a saturn:resource cost; the long form with a type works too.
  • A modifier sits on the tier it settles with: refined's value_multiplier multiplies the currency value of its items by 1.25, and its own description decides whether that extra tooltip line is drawn. common and flawless declare no modifiers, so both behave as 1.

Step 2 — The Ladder's Name Goes on the Entry Tier​

The entry tier is the one nothing writes as its next. Of the three above, nothing points at common, so common is the entry. The ladder's name is the quality field, written on the entry tier, and it follows next to every tier of the ladder: here all three belong to example:pill.

The name is read from the entry tier alone. Move "quality": "example:pill" from common to refined and the entry tier common has no name — so the whole ladder has no name, and /quality chain prints that name as null. The copy on the middle tier only takes part in the "one tier receives two different names" check below. Nothing is reported: no check can tell that you meant to name the ladder.

A ladder's name is just an id, and writing it once per ladder is enough. Only two things about the name are reported:

  • A tier whose own name differs from the one it inherits from above reports names quality <written> inside <inherited> at load.
  • Two entry tiers claiming one name report starts a second ladder named <name>, which another tier already names.

Step 3 — The Price Sits on the Tier You Climb Into​

Going from common to refined reads refined's upgrade_costs and upgrade_condition: that cost is what "climbing into this tier" takes, so the 20 example:qi is written on refined, not on common.

On the source tier it never applies, and nothing is reported: common's upgrade_costs would mean "climbing into common itself", and no tier climbs into an entry tier. That cost is never collected, so the step is in practice free.

  • A tier with no upgrade_costs makes the step into it free; an empty array is free too.
  • To make a tier the top, leave its next out. A tier that writes next without upgrade_costs still has that step, at an empty price.
  • A tier with no next that writes upgrade_costs or an upgrade_condition other than mxt:always reports declares upgrade_costs or upgrade_condition but no next tier.
  • Upgrading is atomic: when the condition fails or the cost cannot be paid, the stack does not move and no tier is written.

upgrade_condition decides whether the step may be walked, and is checked before anything is paid; condition decides whether an item of that tier can be used at all.

Step 4 — Put an Item on the Ladder​

A stack takes the first tier it can get, in this order:

WayHow to write it
The mxt:quality component on the stack/give @s kubejs:qi_pill[mxt:quality="example:common"]; /quality set, a successful upgrade, a Forge Table settlement and a talisman inscription write this one too.
The quality the definition the stack carries declaresWhichever component on that item names a definition identity, read for the tier that definition writes itself.
The default_quality registryWhen a definition's items matches the item, the quality that definition writes applies; a bare creative or /give item and the item-identity-free artifact / spirit_herb definitions take this route.

Layer 2 answers for "several definitions of a type share one built-in item, so the item cannot say which tier applies": technique manuals, pills, furnace specifications, spirit-root stones and physique stones each share one built-in item, and nine definitions declare quality themselves. The list and the carrier component of each are on quality.

The component holds a whole quality object, so it decides both the tier and the ladder it belongs to: example:common puts that pill on example:pill. Binding tables declare no quality, so do not write a ladder into a binding. The forging record mxt:forging_result keeps only the blueprint id and the step counts and never a tier. This pill is an ordinary KubeJS-registered item with no component on it that names a definition, so only layers 1 and 3 can answer for it. When none of the three layers answers, the pill simply has no quality and no ladder's entry tier is supplied for it.

CommandWhat it does
/quality get [player]Shows the tier of the item in the main hand; passing a player needs gamemaster permission.
/quality set <targets> <quality>Writes the override component onto the target's main-hand item; needs gamemaster permission.
/quality clear <targets>Removes the override component; needs gamemaster permission.
/quality upgrade <targets>Walks one step up the ladder; needs gamemaster permission.
/quality chain <quality>Draws the whole ladder, with the ladder's name first; needs no permission.

The top-level alias /quality can be switched off in the Command Aliases tab of the server configuration; /mxt quality keeps working when it is off.

Step 5 — Load and Validate​

Quality is a data pack registry, read once when the world loads, and /reload does not read it again. After editing the files, leave to the title screen and open the world again, or restart the server.

(load the world again)
/mxt registries validate → every problem in one pass
/mxt registries list → mxt:quality=3
/quality chain example:common → the whole ladder, named example:pill
/quality get → the tier of the item in your main hand

/mxt registries validate reports every problem at once, and strings only the first 12 together when there are more. The ladder's own problems show up here: a cycle, a fork, two entries claiming one name. A next pointing at a tier the pack does not provide is not among them — that fails the whole data pack load.

Verify​

  1. Open the world again and check that /mxt registries validate reports nothing.
  2. /give @s kubejs:qi_pill[mxt:quality="example:common"], then /quality get: it shows common.
  3. /quality chain example:common: the name is example:pill and all three tiers sit on that one line in order.
  4. Holding the pill, run /quality upgrade @s: with 20 example:qi on you it goes up to refined, /quality get follows, and the tooltip gains the value_multiplier description line; with fewer than 20 it reports that the cost cannot be paid and the tier does not move.
  5. /quality set @s example:flawless overrides the tier to the top one, and /quality clear @s removes the component again. The pill has no entry in default_quality and carries no component naming a definition, so after the clear /quality get reports that it currently has no quality.
  6. Delete refined's upgrade_costs, open the world again and upgrade once more: that step costs nothing.

Common Mistakes​

SymptomCause
The ladder's name prints as nullThe name was written on a middle tier, so the entry never receives it and the whole ladder is unnamed. Nothing is reported.
The upgrade never collects its costupgrade_costs was written on the source tier, so the cost means "climbing into itself", and nothing climbs into an entry tier. That step is in practice free. Nothing is reported.
names quality <written> inside <inherited>A tier's own name differs from the one inherited from above.
starts a second ladder named <name>, which another tier already namesTwo entry tiers claim one name.
next <id> is not a qualityThe pack never loads that far: a next pointing at a tier that does not exist makes the world refuse to load, so it is a failed load, not "that ladder is dropped".
follows both <A> and <B>One tier is written as the next of two tiers (a fork); reported on the tier being pointed at. Every line running into that fork point is left out of the index whole (the other entry's line included) — no winner is picked for you.
the chain is cyclic at <id>A cycle. A pure cycle whose tiers name nothing reports nothing at all — those tiers are simply on no ladder.
declares upgrade_costs or upgrade_condition but no next tierA tier has no next but writes upgrade_costs or an upgrade_condition other than mxt:always.
cannot be reached from the start of its ladder: it is cyclic, points into another, or names a quality that is already takenA tier names a ladder but cannot be reached from its entry.
Writing a chain field does nothingchain is not a field, so it is silently ignored as an unknown key; quality is the only source of a ladder's name.
Editing the files changes nothingData pack registries are read when the world loads, and /reload does not read them again.

Next​