Repository navigation
feat(items): the tooltip's stat line values as built-in placeholders - #823
Conversation
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
left a comment
There was a problem hiding this comment.
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.
…ooltip-values # Conflicts: # src/source/Engine/Object/ZzzInventory.cpp
A socket count past MAX_SOCKETS read the fields after the sockets as more of them.
…ooltip-values # Conflicts: # tests/data/test_item_tooltips.cpp
|
Thanks, fixed in 5e6fcda. |
Mosch0512
left a comment
There was a problem hiding this comment.
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
WORDhero 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
GetStrengthenCapabilityreturns early without a harmony option and only reads. It's also zero-initialized now, where the oldStrengthenCapability 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).
ComputeItemTooltipValuesalways dereferencesCharacterAttribute(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.
| TooltipHeroScope hero; | ||
| MakeNewDarkWizard(hero.Hero()); | ||
|
|
||
| ItemCreationParams params = FullItemParameters(0, 0); |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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); |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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) |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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; |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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 = { |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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.
|
Thanks for the review. Pushed f408ef6, on top of a merge of main (#822, #804, #860 and the rest):
|
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, asRenderItemInfo()did:damageMin,damageMaxdefensemagicDefense,blockRate,attackSpeed,walkSpeed,magicPowerdurability,maxDurabilityCalcMaxDurabilityiceResistance…waterResistancerequired…forLevel,Strength,Dexterity,Vitality,Energy,Leadershipmissing…for the same sixThe 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'stooltip-snapshotof 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):{magicDamageMin}/{magicDamageMax}from #815's planned list aren't needed: a skill book's wizardry damage is{damageMin}/{damageMax}.Found while doing this
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:
Related issues
Step 5b-2 of #815. Builds on #822, #821, #820, #819, #818, #817, #816 and #813.
Checklist
docs/CODING_RULES.md.docs/build/README.md).tooltip-snapshotof two heroes, identical before and after, and the example above in both themes.