Repository navigation
Item stat profiles: take the level bonus tables from the profile instead of the item's power-ups? #1303
Description
Is your feature request related to a problem? Please describe.
#1290 adds item stat profiles: named sets of item formulas, in the shape of the MuMain client's item stat file, so that both sides can exchange them. A follow-up makes the game logic read them; #1288 checks that it computes the same values as today.
A profile also names a level bonus table for each stat it writes. For example, the profile default names "Defense Increase (Armors)" for the base defense. But today every base power-up of an item already names its own table (ItemBasePowerUpDefinition.BonusPerLevelTable). So after #1290, the level table of an item's stat is named in two places: on the item's power-up and in its profile. The configuration update makes them equal (an item with a table of its own gets a copy of its profile, e.g. thirdTierWing2), but later edits can make them differ, and then one of them is silently ignored.
The question for this issue: for the stats the client knows (damage, magic power, block rate, defense), should the game take the level table from the item's profile, as the client does, or keep taking it from the item's power-up, as today?
How the client does it (MuMain)
An item names its profile, and the profile names one table per stat from the table list of Data/Items/Stats/ItemStats.json. Items don't name tables.
// tables
{ "name": "defenseIncreaseArmors", "values": [0, 3, 6, 9, 12, 15, 18, 21, 24, 27, 31, 36, 42, 49, 57, 66] }
// the profile "default"
"defense": { "table": "defenseIncreaseArmors", "excellent": { "times": 12, "add": 4, "dropLevelDivisor": 5 }, … }
"magicPower": { "table": "magicPowerIncrease", "excellent": { "times": 25, "add": 5 }, "ancient": { "add": 2, "dropLevelDivisor": 60 }, "perLevelAfterHalving": 2 }
// an item, in Data/Items/Group08_Armor.json
{ "number": 9, "name": { "en": "Plate Armor", … }, "statProfile": "armor", … }Magic power has one table for all staffs, sceptres and books ("Magic Power Increase", the same numbers as the armour table). The client adds the table and the excellent and ancient parts to the full magic power, halves the sum once, and adds perLevelAfterHalving × level (2 for staffs and books, 0 for sceptres).
How the server does it today (OpenMU)
- Every base power-up names its table.
ItemPowerUpFactory.GetBasePowerUpWrappersadds the table's value at the item's level to the base value: Plate Armor's "Base Defense" (30) points to "Defense Increase (Armors)", so its defense at +9 is 30 + 27 = 57. - The formulas are in code:
ItemExtensions.GetRequirement(how requirements grow) andItemPowerUpFactory.CreateExcellentAndAncientBasePowerUpWrappers(what excellent and ancient items get more). - Staffs store their rise already halved (Legendary Staff: 59 ÷ 2 = 29.5) and use two tables, "Staff Rise (odd)" and "Staff Rise (even)". They are the client's table halved plus 2 per level, rounded up or down by the parity of the magic power. So the server computes the Legendary Staff at +9 as 29.5 + 32 = 61.5 (excellent 75.5), where the client shows 61 (excellent 76).
The flows
Today:
flowchart LR
item["Plate Armor"] --> powerUp["Base power-up: Base Defense, base value 30"]
powerUp -->|"Bonus Per Level Table"| table["Defense Increase (Armors): +27 at +9"]
code["Formulas in code: ItemExtensions, ItemPowerUpFactory"] --> extras["Requirements, excellent and ancient bonuses"]
After #1290 as it stands, two places name the table:
flowchart LR
item["Plate Armor"] --> powerUp["Base Defense, base value 30"]
powerUp -->|"Bonus Per Level Table (used by the game)"| table["Defense Increase (Armors)"]
item -->|"Stat Profile"| armor["Profile armor"]
armor -->|"laid over"| default["Profile default"]
default -->|"table of Base Defense (only for the exchange)"| table
Proposed, as the client:
flowchart LR
item["Plate Armor"] --> powerUp["Base Defense, base value 30, no table"]
item -->|"Stat Profile"| armor["Profile armor: 5 strength per option level"]
armor -->|"laid over"| default["Profile default: formulas and tables"]
default -->|"table of Base Defense"| table["Defense Increase (Armors)"]
The client:
flowchart LR
item["Item: Plate Armor, statProfile armor"] --> armor["ItemStats.json: profile armor"]
armor -->|"laid over"| default["ItemStats.json: default"]
default -->|"defense.table"| table["tables: defenseIncreaseArmors"]
Describe the solution you'd like
For the stats the client knows, an item only knows its profile, and the profile names the table:
- In the follow-up which lets the game read the profiles: base defense, physical damage, defense rate and shield defense take their level table from the item's profile (laid over
default). The values stay the same, because the configuration update made the tables equal, and the comparison test of test(items): pin today's item requirement, power-up and price formulas for every item #1288 checks it. - A magic power fix, as its own PR afterwards: staffs, sceptres and books compute like the client: the full magic power, the one table "Magic Power Increase", the sum halved once, plus 2 per level (0 for sceptres). That changes values by up to 0.5 to match the client (Legendary Staff +9: 61.5 → 61, excellent 75.5 → 76), and it needs a configuration update for the staffs', sceptres' and books' power-ups.
- A cleanup update: it removes
BonusPerLevelTablefrom the base power-ups of these stats, so that the admin panel doesn't show a table which has no effect.
Other level tables (e.g. the running speed of some armour pieces) stay on their power-ups, until profiles can hold terms for any attribute (#1302). An item which needs a table of its own gets a profile of its own, as the configuration update of #1290 already does.
| Situation | The item's power-up names the table (today) | The profile names the table (proposal, and the client) |
|---|---|---|
| Who decides an item's +level bonus | each power-up of each item | the item's profile, or default where it doesn't write it |
| Change the numbers for all armours | edit the table | the same |
| Give all armours another table | change it on every armour's power-up | change it once, in the profile |
| Give one item its own table | edit its power-up | give the item its own profile (a copy) |
| Create a new item | pick the right table on each power-up | pick a profile; its tables come with it |
| Can two places disagree? | yes, once profiles exist; one is ignored | no, for these stats only the profile names a table |
| Exchange with the client | translate; report or split items whose table differs | one to one |
| Staffs, sceptres, books | two tables per kind, halving built in | one table, computed like the client (after the fix) |
| Other stats with level tables | the same mechanism for everything | still on the power-up until #1302: two mechanisms for a while |
| Work | none | the follow-up, the magic power fix and a cleanup update |
Describe alternatives you've considered
- Keep the power-up's table as the one the game uses, and the profile's table only describes the items for the exchange. Nothing changes in how values are computed; the export reports items whose table differs from their profile's, and the import writes a changed profile table onto the items. Two places stay.
- Move every level table into profiles at once, also running speed and the like. That needs terms for any attribute first (Item stat profiles: excellent and ancient terms for any attribute #1302).
Mockups
- Today: Plate Armor in the admin panel. The level table sits on the item's power-up, and the formulas are in the code.
- After: Plate Armor names its profile "armor". Its power-up keeps the base value; its own table pointer is removed by the cleanup update. ("2b-2" is the follow-up which lets the game read the profiles.)
- After: the profile "default" with its level tables, excellent and ancient terms and requirement formulas, and the two drop level bonuses of the configuration.
- After: the profile "armor" only writes 5 strength per option level. Plate Armor's values worked out, the same as today.
- A level table only holds the numbers per item level, on both sides. Below: the client's one magic power table next to the server's two staff tables.
- The comparison: the item's power-up or the profile names the table.
Additional context
Questions for the maintainers:
- Should the profile name the level table for these stats (the proposal), or should the power-up stay the source?
- Is it fine that the magic power fix changes values by up to 0.5, to match what the client shows?
- Is it fine that a cleanup update removes
BonusPerLevelTablefrom those power-ups?
Related: #1290 (the profiles), #1288 (the comparison test), #1302 (terms for any attribute), #1301 (the books' energy requirement), sven-n/MuMain#861 (the client's item sync), sven-n/MuMain#864 (the client side of #1302).
Activity
Mosch0512 commentedon Oct 26, 2026
A summary of the discussion on Discord (26.10.), so the decision is recorded here:
thirdTierWing2). The follow-up which lets the game read the profiles (fork PR feat(items): the item formulas read the item stat profiles Mosch0512/OpenMU#5, the upstream PR follows) also warns at the start about items which can be worn but have no profile.Questions
Use cases the design should cover (please add any which are missing):
armor.default.Not part of this issue: Stinkyfax would like metadata such as the item tier or rarity, to build drop tables from it. I think that's a concept of its own rather than part of the stat profiles, which group items by how their stats grow (all armour sets share one), so it's worth its own issue.
ze-dom commentedon Oct 27, 2026
Regarding number 2, it might be a precedent for turning every stat in the server as the client shows it (hp, mana, etc.), that is, rounded off. Not saying it's a negative thing, but it's a design decision which apparently @sven-n chose not to follow from the start, at least. The trend seems to be that the client and server will be evermore tightly coupled, so it might need to be revisited :)