Skip to content

feat(items): the descriptions' repeated texts are written once - #827

Open
ezequielzacca wants to merge 233 commits into
sven-n:mainfrom
ezequielzacca:feat/item-tooltip-shared-texts
Open

ezequielzacca wants to merge 233 commits into
sven-n:mainfrom
ezequielzacca:feat/item-tooltip-shared-texts

Conversation

@ezequielzacca

@ezequielzacca ezequielzacca commented Oct 21, 2026 •

Copy link
Copy Markdown
Contributor

This step's own changes: compare with #825. The diff of this PR also holds every earlier step of #815, since a PR from a fork can only target this repository's branches.

Summary

Step 5b-4 of #815 (data-driven item tooltips; design). Stacked on #825 (→ #824 → … → #813): only the last commit, feat(items): the descriptions' repeated texts are written once, is this step.

The descriptions moved into data in steps 2–5a repeated themselves. There were 263 text lines but only 147 distinct texts, each copy with its translations. This step writes each repeated text once. No tooltip changes.

Shared texts

25 texts that several items repeated are shared texts of Tooltip.json now, and each line names one ("text": "@increaseSpeed"). That turns 150 copies into one each. The most repeated:

  • "Increase speed" ×22;
  • the CS team message ×12;
  • "You can achieve special items with combinations." ×11;
  • "Automatic HP recovery 3%" ×8;
  • "It's a box containing various items." ×6.

The line keeps its own color, levels and conditions. For example, "Automatic HP recovery 3%" is blue on some items and white on others.

A line's own values

The wings, capes and pets had ten versions of "Increase/Absorb {table}% of Damage", differing only in the table name. A description line can now name the value of its placeholder, as a layout line does since #824:

{"text": "@damageIncrease", "values": {"percent": "wingDamageIncrease3rd"}},
{"text": "@damageAbsorb", "values": {"percent": "wingDamageAbsorb3rd"}}

That gives two shared texts, "Increase {percent}% of Damage" and "Absorb {percent}% of Damage", where there were ten.

Values by item level

An item can give its description's own placeholders a value for each item level. A value is a text with its translations, or a shared text. The Fruits had two lines per stat at each of their five levels, and now have two lines:

"description": [
  {"text": {"en": "{stat} Increases 1~3 stat points", …}, "levels": [0, 4]},
  {"text": {"en": "{stat} Possible to decrease stat 1~9 point", …}, "levels": [0, 4]},
  …
],
"levelValues": {"stat": [{"en": "ENG", "de": "ENG", …}, {"en": "STA", "de": "VIT", …}, …, {"en": "Command", "de": "Führung", …}]}

Placeholders can therefore be text as well as numbers now. The same mechanism can later carry the Talisman of Mobility's saved location, or the Transformation Ring's monster by level (step 5d).

Checks

Errors when the data loads, with the meant name suggested for a typo:

  • a line's values naming neither a built-in value nor a table;
  • a level value named like a built-in value or a table;
  • a level value with placeholders, or a missing @text;
  • a line that shows a level value at a level the value doesn't have.

A level value that no line shows is a warning. Shared texts in descriptions go through the same placeholder checks as texts written in place.

Not done, as discussed in #815

  • Profile-bound values. The design had each stat profile name the table a placeholder means. But items that share a profile use different tables:

    • thirdTierWing's wings and its capes 12/40 and 12/50;
    • smallWing's wings and its small capes;
    • the two secondTierCape capes.

    So the line's own values does it, without splitting profiles.

Verified

  • "Every item's tooltip is as recorded" is unchanged for every item, at every level, in all six variants and for both heroes. It records English only.
  • The game's tooltip-snapshot is identical before and after for the Dark Knight in English, German and Russian, and for the Dark Wizard in English: 91,104 tooltips each.
  • The conversion script also checked that every translation is rebuilt exactly.

Found while doing this

Several translations of the wing texts carry English leftovers. Examples:

  • Spanish: "Aumenta el daño de {percent}% of";
  • Portuguese: "Absorver Dano {percent}% of";
  • Russian: "Увеличение {percent}% of урона".

Some Fruits stat words are machine-translation slips:

  • Indonesian: "Bahasa Inggris" ("English language") for ENG;
  • Chinese: "通用人工智慧" ("artificial general intelligence") for AGI.

Each is one shared text or one list now, so each fix is one place. They're left as they were, to keep this PR free of tooltip changes.

Screenshots

The same tooltips before (#825) and after this PR, for a level 400 Dark Knight. They're identical, because only the data behind them changed. The data under each pair is shown in English only; the files keep every translation.

Wing of Storm +15

The shared @damageIncrease and @damageAbsorb, each with the wing's table as the line's values, and the shared @increaseSpeed.

Legacy theme Modern theme
Before
After
The item's data (English only)
{
  "number": 36,
  "name": "Wing of Storm",
  "tags": [
    "valuable"
  ],
  "description": [
    {
      "text": "@damageIncrease",
      "values": {
        "percent": "wingDamageIncrease3rd"
      }
    },
    {
      "text": "@damageAbsorb",
      "values": {
        "percent": "wingDamageAbsorb3rd"
      }
    },
    {
      "text": "@increaseSpeed"
    }
  ]
}

Its shared texts in Tooltip.json (English):

"damageAbsorb": "Absorb {percent}% of Damage"
"damageIncrease": "Increase {percent}% of Damage"
"increaseSpeed": "Increase speed"

Command Fruits +4

{stat} comes from the item's levelValues: "Command" at +4 ("Führung" in German). Ten lines are two now.

Legacy theme Modern theme
Before
After
Before, German
After, German
The item's data (English only)
{
  "number": 15,
  "name": "Fruits",
  "tags": [
    "valuable"
  ],
  "description": [
    {
      "text": "{stat} Increases 1~3 stat points",
      "levels": [
        0,
        4
      ]
    },
    {
      "text": "{stat} Possible to decrease stat 1~9 point",
      "levels": [
        0,
        4
      ]
    },
    {
      "text": "It can be used with item removed.",
      "color": "darkRed",
      "levels": [
        0,
        4
      ]
    },
    {
      "text": "Can be equipped by Dark Lord",
      "levels": [
        4,
        4
      ]
    }
  ],
  "levelValues": {
    "stat": [
      "ENG",
      "STA",
      "AGI",
      "STR",
      "Command"
    ]
  }
}

Related issues

Step 5b-4 of #815. Builds on #825, #824, #823, #822, #821, #820, #819, #818, #817, #816 and #813.

Checklist

  • I have read and followed docs/CODING_RULES.md.
  • I have built the project locally (see docs/build/README.md).
    • Windows x64 Release: all 1007 tests pass.
    • The new ones cover a line's values, level values and their checks.
    • The recorded tooltip test is unchanged.
    • Changed lines pass clang-format 21.1.8 as CI checks them.
    • In game: tooltip-snapshot before and after in three languages, and the screenshots above in both themes.
  • Changes are focused on one concern; the diff is as small as it reasonably can be.

…m their data

The damage increase and absorption the tooltip code computed for the
wings, capes and pets are their descriptions now, with placeholders over
11 new level bonus tables of Stats/ItemStats.json (wingDamageIncrease1st
is 12 + 2 per level, ...): 24 wings and capes, plus Horn of Dinorant,
Guardian Angel and Imp, whose fixed numbers are written in the text.
Their cases leave RenderItemInfo; the two .resx texts no code uses any
more go too (the shared "Increase %d%% of Damage" and "Increase speed"
stay, other code prints them).

The printf texts become data as the game showed them: %d is the
placeholder or the number, %% is %. Two translations of the Imp's text
had broken escapes ("% %", shown as %; Portuguese "% o", read as an
octal number); they read as plain text now.

A table no stat uses is no longer a warning of the stat file alone: it
can be one for descriptions, so ValidateDescriptionPlaceholders warns
about a table that neither a stat nor a description uses.

A test compares every one of the 27 items at every level from +0 to +15
with the old formulas.
A description line can be limited to item levels: "levels": [1, 8]
shows it from +1 to +8 (both included; left out, at every level). The
tooltip draws only the lines of the item's level.

With it, the last fixed texts of RenderItemInfo's item chain are
descriptions: Town Portal scroll (+1 to +8), Spirit, Loch's Feather,
Fruits, Siege Potion, Contract Summon, Life Stone and Rena by level,
and Scroll of Archangel, Blood Bone and Invisibility Cloak, which have
one text at every level. Their cases go, and so do the 18 .resx texts
no other code prints (no Lookup() range reaches their legacy ids).

The old code drew a stale line of the text list at the Contract
Summon's, Life Stone's and Rena's levels without a text (+2 and up,
Rena +0); those levels show none now. Otherwise nothing changes: a test
compares every level of the 11 items with the old code's lines and
colors.

The Armor of Guardsman's Chaos Castle table and the Lost Map / Symbol
of Kundun stay code: they read the hero and other tables.
…data

The fixed lines of the item chain RenderItemInfo writes under an item's
name (seals, talismans, tickets, cards, rings, necklaces, keys, boxes,
pets, cherry blossoms, ...) are the descriptions of 72 items now: 55
cases with only fixed texts, colors, blank lines and constant numbers.
Their texts keep all 11 UI languages, converted as the code printed
them (printf %% as %, its numbers and text arguments written in; a %ls
text as it is; a translation without the %d keeps its own number, as
printf left the argument out). The Moonstone Pendant's "Cannot Repair"
comes before its description.

None of the 72 items has stat lines or a box list, which are drawn
between the chain and the description, so the tooltips look the same;
a test compares each item's lines and colors with the old code's.

The cases go (527 lines), and so do the 51 .resx texts no other code
prints (none is in a computed Lookup() range: 2500-2501, 2503-2508,
2512-2516, 2732-2741).

The other 34 cases stay code: values of the item (durability, the
item add options), timers and maps, level switches, bold lines, helper
conditions, and the four cards whose legacy texts the game does not
have (Lookup() shows them as empty lines).
RenderItemInfo() is two functions now: BuildItemTooltipText() writes the
tooltip's lines into TextList / TextListColor / TextBold as before, and
RenderItemInfo() draws them (the Dark Lord's pets still draw their own).
The code only moves, in the same order: the early returns of the item,
the pet check and the set and socket lines stay in the builder, the
height and position of the box in the drawing. Nothing changes in game.

BuildItemTooltipLines() returns the built lines without drawing, and two
control socket commands use it (in builds with the control socket):
- `tooltip` gives the lines of an item made from the fields the server
  sends (level, luck, skill, option, excellent, ancient, harmony);
- `tooltip-snapshot` writes the lines of every item at every level, as
  plain, full, ancient and harmony items, for the hero as he is, to a
  text file: 60,736 tooltips in about a second, the same every run, to
  compare the tooltip code before and after a change.

This is step 5b-1 of sven-n#815: the snapshot is the check for moving the stat
lines into data.
The tooltip lines of every item now build in a test, without the game's
windows or fonts:
- Items are made with FillItemFromParameters, the part of the item
  manager's CreateItemByParameters that needs no item list or key.
- Without the inventory window an item counts as worn (as an item the
  inventory grid does not hold always did).
- A test can measure description text with a fixed width
  (SetItemTooltipTextMeasure) instead of the game's font.
- The test loads the game data the tooltip reads as the game does:
  skills, socket items, item sets, monster names, the Jewel of Harmony
  and add option tables, and the buff system's time texts.

"Every item's tooltip is as recorded" builds the 60,736 tooltips of
tooltip-snapshot for two heroes (a new Dark Wizard, a level 400 Blade
Master) and compares a hash per item with item_tooltip_snapshot.txt;
a change prints the item's new lines. MU_TOOLTIP_SNAPSHOT_UPDATE=1
records the hashes after a change that is meant. The snapshot code is
shared with the control socket's tooltip-snapshot
(Engine/Object/ItemTooltipSnapshot).

The test's snapshot equals the game's for the same hero, apart from the
wrapped description lines (the font). That comparison found one bug: a
Transformation Ring above +5 read past SommonTable (six monsters, +0 to
+5), so its name was whatever memory held there ("Bull Fighter" at +7 in
the game, another monster in the test). Such a ring has no monster, and
so an empty name, as at the other levels above +5.
ComputeItemTooltipValues (Engine/Object/ItemTooltipValues) computes the
numbers the tooltip's stat lines show, for an item and the hero, as
RenderItemInfo did: damage (with the Jewel of Harmony's attack bonus; a
skill book's wizardry damage; the minimum lifted to the maximum shown
twice), defense (with its defense bonus), magic defense, block rate,
attack and walk speed, magic power, durability and its maximum, the
seven resistances (the item's level + 1 for one it has), and for the
level, strength, agility, vitality, energy and command requirements
what the item asks (after the Jewel of Harmony's and the sockets'
strength and agility reductions) and what the hero lacks. The states
the lines' colours follow come with them: excellent, ancient, the
harmony bonuses and a reduced requirement.

RenderItemInfo writes those numbers instead of computing its own, so the
recorded tooltip test checks every value of every item for both heroes:
it is unchanged, and so are the game's snapshots of two real heroes.

The values are built-in placeholders of the descriptions now: {damageMin},
{damageMax}, {defense}, {magicDefense}, {blockRate}, {attackSpeed},
{walkSpeed}, {magicPower}, {durability}, {maxDurability}, {iceResistance}
... {waterResistance}, {requiredLevel} ... {requiredLeadership} and
{missingLevel} ... {missingLeadership}. The checks know their names (and
suggest them for a typo); the tooltip fills them in for the item.

The sockets' requirement reductions (seeds 38 and 39) move as they were;
the shipped Season 6 socket data has no option at those seeds, so no item
reaches them.
The tooltip's damage, defense, magic defense, defense rate, attack and
walk speed, resistance and requirement lines ("(lacking ...)" included),
and the staves', books' and magic swords' wizardry or curse damage
increase and the sceptres' pet attack increase, are data now:
Data/Items/Tooltips/Tooltip.json holds their texts with translations and
the layout "default", which every item uses. RenderItemInfo writes the
layout's lines between the parts that stay code: the name, the item's own
texts and durability, the classes, and the options.

A line shows when every entry of its "when" holds: a built-in value that
is not 0, a state (excellent, twoHanded, skillBook, ...), or an item tag
("tag:sceptre"); "!" turns an entry around. Its color is the first of its
color rules that holds, else its own; "values" binds a placeholder of the
text for the line, so one "(lacking {missing})" serves every requirement.
Unknown texts, conditions, tags, colors and placeholders, translations
with other placeholders, and misplaced sections are load errors, with the
name meant suggested for a typo.

Two item checks of the code are tags: noDamageLine (the Scrolls of
Teleport, Teleport Ally and Soul Barrier) and noLevelRequirement (Loch's
Feather).

The recorded tooltips change for 11 items, both from the old damage
block's TextNum-- when it skipped the line: the eight skill scrolls whose
skill has no damage keep the blank line under their name now, and the
excellent teleport scrolls no longer color that blank line.

The .resx texts only these lines used are gone; the ones other windows
use stay.
Every item's recorded tooltip was at full durability and not timed, so
the durability lines of a stack's count (potions and jewels have no
durability in their data) and the lines a timed item leaves out were
never recorded. Two more variants record them: "count", at a durability
of 3, and "timed", an expired timed item (one that is not expired shows
the time it expires, which depends on the clock).

The hashes are recorded again for the six variants; the tooltip code is
unchanged.
The standard durability line, "Durability: [{durability}/{maxDurability}]",
is a line of the default tooltip layout: shown for an item with a
durability (the old rule, now the state hasDurability) that is not timed
and has no tag noDurabilityLine. The other durability lines are lines of
the items' descriptions: a stack's "Number of items", a pet's "Life", a
ticket's "usable ... times", the Splinter of Armor's "{durability} / 20",
the Transformation Ring's, the Talisman of Luck's, the Varka tickets' and
the Talisman of Mobility's texts, the wing-mix charms' texts and the
lucky item tickets' text. The 57 items with a durability but not the
standard line have the tag noDurabilityLine; the condition with about 20
exceptions is gone.

Description lines take a shared text of the tooltip file ("@life") and
conditions ("when", as a layout line's). A condition can compare a value
with a whole number: "durability=2", "level!=1". A description's shared
text that the tooltip file does not have, or that has a placeholder a
description cannot fill, is a load error; a shared text that neither a
layout nor a description uses is a warning.

Only the Talisman of Mobility's saved location stays code, under its
description's line.

The recorded tooltips of every item, now also with a count and timed,
are unchanged except for the 10 lucky item tickets: the old code wrote an
empty full-height line after their blank line, and they end with the
blank line now.

The .resx texts only these lines used are gone, as is IsCharacterCard,
which nothing calls now.
The tickets' "usable %dtimes" ran the number into the word ("usable
10times") in English, Spanish and Polish, left "times" in English in
Russian and Tagalog, and read "time 10 that can be used" in Indonesian.
The shared text of the tooltip file and the .resx text, which the Chaos
Castle ticket and the Santa Village invitation still use, are fixed in
those languages.
The 25 texts that several items' descriptions repeated ("Increase speed"
22 times, the CS team message 12 times, ...) are shared texts of the
tooltip file now, which the lines name ("@increaseSpeed"): 150 copies with
their translations become one each.

A description line names the value of its own placeholder, as a layout
line does: the wings', capes' and pets' ten "Increase/Absorb {table}% of
Damage" texts are the shared texts "Increase {percent}% of Damage" and
"Absorb {percent}% of Damage", with "values": {"percent": "<table>"} on
each line.

An item gives its description's own placeholders a value by item level,
a text with its translations or a shared text ("levelValues"): the Fruits'
ten lines, two for each of their five stats, are two lines over {stat}.

The placeholder checks know the line's values and the item's level
values: a line value that is neither a built-in value nor a table, a
level value named like one, with placeholders or missing at a level its
line shows, is an error; a level value no line shows is a warning. The
tooltips are unchanged.
…at/item-tooltip-values-to-data

# Conflicts:
#	docs/item-data.md
#	src/Localization/Game.pl.resx
#	src/source/Data/GameData/ItemData/ItemStatProfiles.cpp
#	src/source/Data/GameData/ItemData/ItemStatsJson.cpp
…/item-description-levels

# Conflicts:
#	src/Localization/Game.pl.resx
…m-tooltip-chain-texts

# Conflicts:
#	src/Localization/Game.pl.resx
…ltip-context

# Conflicts:
#	tests/data/CMakeLists.txt
…oltip-layouts

# Conflicts:
#	src/Localization/Game.pl.resx
…m-tooltip-shared-texts

# Conflicts:
#	src/bin/Data/Items/Group13_Helper.json
#	src/bin/Data/Items/Group14_Potion.json
The tooltip and tooltip-snapshot commands this branch adds were missing from
the test's list, so the editor build's "Control protocol serves the
documented command vocabulary" counted 36 commands against 34.
@ezequielzacca

Copy link
Copy Markdown
Contributor Author

Thanks. Both are covered now.

…or every language and the levels

Three Polish Fruits texts were from before main corrected them (17fc723): Dowodzenie, "Można użyć po zdjęciu
ekwipunku." and "Może być używany przez: Dark Lord". A test now compares the level lines whose texts other code still
prints with the game's texts in every language, another that a line is only written at its levels. The levels note
says the level variants' lines move into their items later.
…m-tooltip-chain-texts

# Conflicts:
#	tests/data/test_item_database_files.cpp
…ooltip-durability

# Conflicts:
#	docs/item-data.md
…m-tooltip-shared-texts

# Conflicts:
#	src/bin/Data/Items/Group13_Helper.json
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants