Skip to content

feat(items): the tooltip's stat line values as built-in placeholders - #823

Merged
Mosch0512 merged 31 commits into
sven-n:mainfrom
ezequielzacca:feat/item-tooltip-values
Oct 25, 2026
Merged

Mosch0512 merged 31 commits into
sven-n:mainfrom
ezequielzacca:feat/item-tooltip-values

Conversation

@ezequielzacca

@ezequielzacca ezequielzacca commented Oct 21, 2026 •

Copy link
Copy Markdown
Contributor

This step's own changes: compare with #822. 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-2 of #815 (data-driven item tooltips; design). Stacked on #822 (→ #821 → … → #813): only the last commit, feat(items): the tooltip's stat line values as built-in placeholders, is this step.

The values the tooltip's stat lines show are computed in one place now, and the item descriptions can show them.

The values

ComputeItemTooltipValues() (Engine/Object/ItemTooltipValues) computes them for an item and the hero, as RenderItemInfo() did:

Value As the tooltip computes it
damageMin, damageMax with the Jewel of Harmony's attack bonus (a minimum lifted to the maximum shows the maximum twice); a skill book's wizardry damage from its skill
defense with the Jewel of Harmony's defense bonus
magicDefense, blockRate, attackSpeed, walkSpeed, magicPower as they are
durability, maxDurability CalcMaxDurability
iceResistance … waterResistance the item's level + 1 for a resistance it has, else 0
required… for Level, Strength, Dexterity, Vitality, Energy, Leadership after the Jewel of Harmony's and the sockets' strength and agility reductions
missing… for the same six what the hero lacks (0 when nothing)

The states the lines' colors follow come with them: excellent, ancient, the harmony bonuses, and a reduced requirement. They're for 5b-3's layouts.

RenderItemInfo() writes these numbers instead of computing its own. Its branches and quirks stay as they were, for example the teleport scrolls' missing damage line. So "Every item's tooltip is as recorded" (#822) checks every value of every item for both heroes, and it's unchanged. The game's tooltip-snapshot of two real heroes (local OpenMU) is identical before and after too.

As placeholders

They're built-in placeholders of the descriptions now, next to {level} (#817):

  • the description checks know their names, and suggest them for a typo;
  • the tooltip fills them in for the item;
  • a test checks that every built-in name has a value, so the names and the values can't drift apart.

{magicDamageMin} / {magicDamageMax} from #815's planned list aren't needed: a skill book's wizardry damage is {damageMin} / {damageMax}.

Found while doing this

  • Sockets 38 and 39: the requirement reductions read these seeds, but the shipped Season 6 socket data has no option at either one (seed 36, "Health increase", is the last Earth option). So no item reaches them. They move as they were.
  • 5b-3's layouts need a bold flag: the staff and sceptre magic power lines are bold.

Screenshots

The equipped Divine Sword of Archangel +15 with an example description in the screenshot's local item data only, not in this PR:

{"text": {"en": "Deals {damageMin} to {damageMax} damage, at {durability} of {maxDurability} durability", …}, "color": "blue"}

It shows the same numbers as the stat lines above and below it:

Legacy theme Modern theme

Related issues

Step 5b-2 of #815. Builds on #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 992 tests pass. The new ones are "Every built-in placeholder but {level} is a value of the tooltip's stat lines" and "The tooltip's values of an item follow the item and the hero"; the recorded tooltip test is unchanged.
    • Changed lines pass clang-format 21.1.8 as CI checks them.
    • In game: tooltip-snapshot of two heroes, identical before and after, and the example above in both themes.
  • Changes are focused on one concern; the diff is as small as it reasonably can be.

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.
…ooltip-values

# Conflicts:
#	src/source/Engine/Object/ZzzInventory.cpp
…ooltip-values

# Conflicts:
#	src/source/Engine/Object/ZzzInventory.cpp
#	tests/data/test_item_descriptions.cpp
…ooltip-values

# Conflicts:
#	docs/item-data.md
#	src/source/Data/GameData/ItemData/ItemDataValidation.cpp
#	tests/data/test_item_description_placeholders.cpp
…ooltip-values

# Conflicts:
#	src/source/Data/GameData/ItemData/ItemDescriptionPlaceholders.h
#	tests/data/test_item_description_placeholders.cpp

@sven-n sven-n left a comment

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Guard socket-count bounds when computing requirement reductions. src/source/Engine/Object/ItemTooltipValues.cpp, ComputeRequirements, iterates for (int socket = 0; socket < item.SocketCount; ++socket) and indexes item.SocketSeedID[socket] without checking the array bound. If an invalid or corrupted item has a socket count larger than the fixed array, tooltip construction reads out of bounds. Please clamp/validate the count against std::size(item.SocketSeedID) (or the repository's socket-count constant) and test malformed item metadata. This is especially important now that tooltip computation is reusable outside the original rendering path. Reviewed against AGENTS.md / CODING_RULES.md; not runtime-tested.

@ezequielzacca

Copy link
Copy Markdown
Contributor Author

Thanks, fixed in 5e6fcda. ComputeRequirements now loops over std::min<int>(item.SocketCount, MAX_SOCKETS) sockets. The values test adds an item with SocketCount = 255, no seeds in its five sockets, and the strength seed (38) in SocketSphereLv, the array right after them in ITEM. Before the fix, the loop read that array as a sixth socket and reported reduced strength; now it doesn't.

@Mosch0512 Mosch0512 left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Verdict: changes requested for one item; the rest is non-blocking. The values move is right:

  • The stat lines print the same numbers as before. I compared each branch with the old code:

    • Damage: the harmony bonus at the same level condition, a minimum lifted past the maximum shown as the maximum twice, the skill books' wizardry damage, and the teleport scrolls' colour quirk.
    • Defense, speeds and durability: same values, same colours.
    • Resistances: the same value; level + 1 is never 0, so the condition is the same.
    • Requirements: the shown value, the "lacking" amount and the yellow colour for a reduced requirement, including the old WORD hero stats.

    The only change is the socket bound after sven-n's review.

  • The recorded snapshot agrees. Merged with main (and with the fix below), "Every item's tooltip is as recorded" passes unchanged, so every tooltip of both heroes is the same.

  • Harmony lookups: asking the Jewel of Harmony for the defense bonus of every item is safe, since GetStrengthenCapability returns early without a harmony option and only reads. It's also zero-initialized now, where the old StrengthenCapability SC; wasn't.

  • The placeholders: the 29 names match between the checks and the values, the test pins one direction, and only the game's tooltip fills descriptions, so no other path leaves {damageMin} unfilled.

Required

  • Merged with main, the new values test doesn't compile (inline, test_item_tooltips.cpp:397).

Non-blocking

  • The new file's free functions aren't in a namespace (inline, ItemTooltipValues.h:52).
  • Nothing tests a reduced requirement (inline, ItemTooltipValues.cpp:81).
  • ComputeItemTooltipValues always dereferences CharacterAttribute (inline, ItemTooltipValues.cpp:115).
  • The 29 names live in two lists (inline, ItemDescriptionPlaceholders.cpp:17).

Before merging (not code)

  • The branch needs main. It's built on #822's old head (f6896ff), without #822's review fixes, #860 or the later main merges. GitHub shows it as mergeable because nothing conflicts as text, and the green CI ran on 2026-10-24 at 08:24, before #822 reached main. A merge of main is needed anyway for the item above.

What I checked:

  • every branch of the old stat-line code against ComputeItemTooltipValues;
  • the Jewel of Harmony lookup for kinds 0 to 2 on items without the option;
  • the placeholder names in the checks, the values and the docs, and every caller of FillPlaceholders;
  • a local merge of main into this branch, built on Windows x64 Debug with the editor and the control socket.

As merged, test_item_database_files doesn't build (below). With that one call qualified, it passes whole, 179 of 179, in file order and three random orders, and so do test_item_database, test_control_tooltip_commands and test_control_protocol. I didn't run the game.

Comment thread tests/data/test_item_tooltips.cpp Outdated
TooltipHeroScope hero;
MakeNewDarkWizard(hero.Hero());

ItemCreationParams params = FullItemParameters(0, 0);

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Required. Since #822's review fixes, main has this function only as Engine::Items::TooltipSnapshot::FullItemParameters. With main merged into this branch, the build stops here:

test_item_tooltips.cpp(394): error C3861: "FullItemParameters": identifier not found

The other calls in this file are qualified on main already (the durability test's, for example). Merging main and writing Engine::Items::TooltipSnapshot::FullItemParameters(0, 0) here fixes it. With that change, the merge passes every test I ran.

The merge matters for a second reason: on its own base, this branch still has #822's first TooltipDataScope, which loaded the monster names through OpenMonsterScript. This test is a third user of that scope, so in a whole-binary run the second load overflows the monster table, as #822's did before e9491df. Main's scope loads them through g_MonsterDataHandler (#860), which is safe.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in f408ef6: main is merged in and the call is Engine::Items::TooltipSnapshot::FullItemParameters(0, 0). With the merge the tests use main's TooltipDataScope, which loads the monster names through g_MonsterDataHandler, so the third user of the scope no longer overflows anything. test_item_database_files passes whole in file order and random orders.

};

// The values of `item` for the hero as he is (CharacterAttribute).
ItemTooltipValues ComputeItemTooltipValues(const ITEM& item);

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Non-blocking. These are free functions extracted from RenderItemInfo into a new file. CODING_RULES.md, "Namespaces and file names for extracted free functions (new files only)", asks for them to be wrapped in a <Layer>::<Concern> namespace. #822's snapshot helpers went into Engine::Items::TooltipSnapshot; Engine::Items::TooltipValues would match.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done in f408ef6: ComputeItemTooltipValues and GetItemTooltipValue (plus the new GetItemTooltipValueNames) are in Engine::Items::TooltipValues. The ItemTooltipValues struct stays where it is, since the rule is about the extracted functions and the later PRs use the type.

values.hasDamage = damageMin > 0;
}

ItemTooltipValues::Requirement HeroRequirement(int required, int heroValue, int reduction)

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Non-blocking. The reduced requirements are the most rewritten part, and nothing tests them: the Jewel of Harmony's lower-strength and lower-agility options (weapon options 3 and 4, staff options 2 and 3), shown lower and in yellow.

  • The recorded snapshot's harmony variant is option 1, an attack or defense bonus.
  • The values test has no harmony item.
  • The socket seeds 38 and 39 can't occur with the shipped data, as you found.

So if 5b-3's layouts broke the reduction or the reduced flag, every test would still pass. A values-test case with a weapon at harmony option 3, level 13, checking strength.required == RequireStrength - SI_force and strength.reduced, would pin it.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Added in f408ef6: the values test makes a Kris +13 with the Jewel of Harmony's option 3 at level 13, asks the harmony table for the option's amount, and checks strength.required == RequireStrength - SI_force with strength.reduced set, and agility unchanged and not reduced.

}
}

const CHARACTER_ATTRIBUTE& hero = *CharacterAttribute;

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Non-blocking. This dereferences CharacterAttribute for every item, while the old code read it only for items with requirements. In the game it's always set. But the values are now a public function meant for 5b-3's layouts, so a future caller without a hero, such as a test or an editor preview of a layout, would get a null dereference even for an item without requirements. A null check (no hero: nothing missing), or the precondition stated in the header, makes that explicit.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done in f408ef6: without a hero the requirements are still filled and nothing is missing; the header says so. A test sets CharacterAttribute to null and computes a Kris's values.

{
constexpr std::array<std::string_view, 1> BuiltIns = {LevelPlaceholder};
// {level}, and the values of the tooltip's stat lines, which the game computes (Engine/Object/ItemTooltipValues).
constexpr std::array<std::string_view, 30> BuiltIns = {

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Non-blocking. The 29 stat-value names are listed here for the checks and again in GetItemTooltipValue's tables for the values. The new test checks that every built-in has a value, but not the other way round. A name added to GetItemTooltipValue and forgotten here makes every description that uses it fail the load-time check as an unknown placeholder, although the game could fill it. One list generated from the other, or the test run in both directions, keeps them together.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done in f408ef6: the value names are now file-scope tables that both GetItemTooltipValue and a new GetItemTooltipValueNames read, and the test checks the other direction too: every name the game can fill must be in the placeholder checks' list.

- The values test qualifies FullItemParameters (Engine::Items::TooltipSnapshot
  on main), and main is merged in: with sven-n#860 the tooltip tests load the monster
  names through g_MonsterDataHandler.
- ComputeItemTooltipValues and GetItemTooltipValue are in
  Engine::Items::TooltipValues.
- Without a hero the requirements are shown and nothing is missing, instead of
  reading through a null CharacterAttribute.
- The value names live in tables GetItemTooltipValueNames lists, and the test
  checks them against the placeholder checks' list both ways.
- A test pins a Jewel of Harmony reduced strength requirement: lowered by the
  option's amount and marked reduced.
@ezequielzacca

Copy link
Copy Markdown
Contributor Author

Thanks for the review. Pushed f408ef6, on top of a merge of main (#822, #804, #860 and the rest):

  • Required: the values test calls the qualified FullItemParameters, and the merge brings main's monster-name loading into the tooltip tests.
  • All four non-blocking points done: the Engine::Items::TooltipValues namespace, a test for a harmony-reduced requirement, no null dereference without a hero (with a test), and the value names checked against the placeholder list both ways.
  • Checked: Windows x64 Release with the editor and the control socket: all 1442 tests pass, and test_item_database_files passes whole in file order and random orders. The recorded tooltip snapshot still matches.

@Mosch0512
Mosch0512 merged commit b9a159a into sven-n:main Oct 25, 2026
19 checks passed
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