Skip to content

M7.4 — Item Tooltips

What you'll learn

  • How to show a rich item tooltip on hover — name in rarity color, base type, and one line per affix.
  • Why a custom tooltip Control (a RichTextLabel with BBCode) beats Godot's built-in plain-text tooltip for items.
  • How to render an affix as readable text from its StatModifier data ("+12 armor", "8% increased attack speed").
  • How to add a compare-to-equipped view so the player sees the stat delta against what they're wearing.

How it applies

  • The tooltip is where the player decides. Loot management is reading tooltips and choosing. A clear tooltip — rarity color, legible affix lines, a comparison to current gear — is the difference between confident decisions and squinting. In a genre about evaluating items, the tooltip is a primary interface, not a nicety.
  • Affix text must match the affix data exactly. Because the tooltip renders from the same StatModifiers that are mechanically applied (M5.4/M6.1), what the player reads is what the item does — no separate description string to fall out of sync. The single-source-of-truth discipline reaches the tooltip text.
  • Comparison is the genre's killer feature. "+4 damage, −2 armor versus equipped" turns a wall of numbers into a decision. Computing the delta against the equipped item is exactly the recompute math (M5.4) run hypothetically, reusing logic already built.
  • Rich text is a UI skill. BBCode in a RichTextLabel (colors, bold, per-line formatting) is how you make a dense tooltip readable. Knowing it covers tooltips, combat logs, dialogue, and any formatted text the game shows.

Concepts

Why a custom tooltip

Godot Control nodes have a built-in tooltip_text, but it is plain text — no color, no per-line formatting, no layout. An item tooltip needs the name in its rarity color, the base type, and a list of affix lines, possibly with a comparison block. That calls for a custom tooltip: a small Control scene (a Panel containing a RichTextLabel) you position near the cursor and fill with BBCode.

Two ways to hook it: override _make_custom_tooltip on the slot to return your tooltip Control (Godot shows it automatically on hover), or manage a shared tooltip node yourself on mouse_entered/mouse_exited. The override is the cleaner default for static content; a managed node is handier when the tooltip must update live (e.g., comparison against the currently-hovered-over equipment). This book uses _make_custom_tooltip.

Rendering affixes as text

Each affix is a StatModifier (stat, type, amount). Turn it into a line the player reads, formatting by type: flat as "+N stat", increased/more as "N% increased/more stat". A small formatter keeps the text consistent with the data.

Example

A formatter from StatModifier to a display line:

static func affix_line(m: StatModifier) -> String:
    var stat_name := str(m.stat).capitalize()
    match m.type:
        StatModifier.Type.FLAT:
            return "+%d %s" % [int(m.amount), stat_name]
        StatModifier.Type.INCREASED:
            return "%d%% increased %s" % [int(round(m.amount * 100.0)), stat_name]
        StatModifier.Type.MORE:
            return "%d%% more %s" % [int(round(m.amount * 100.0)), stat_name]
    return ""

Flat shows a raw "+N"; increased/more show a percentage (the fraction 0.20 becomes 20%). Because the line is computed from the same StatModifier the game applies, the displayed value can never differ from the mechanical effect — the tooltip is a view of the data, not a hand-written description.

Building the tooltip BBCode

Assemble the name (in rarity color), base type, and affix lines into BBCode for a RichTextLabel with bbcode_enabled = true:

Example

static func item_bbcode(item: ItemData) -> String:
    var color := Rarity.COLORS[item.rarity].to_html(false)
    var lines := PackedStringArray()
    lines.append("[b][color=#%s]%s[/color][/b]" % [color, item.display_name])
    lines.append("[i]%s[/i]" % Rarity.tier_name(item.rarity))
    for m in item.all_modifiers():
        lines.append("[color=#9fb4ff]%s[/color]" % affix_line(m))
    return "\n".join(lines)

The name uses the rarity color from the shared table (M6.2), so the tooltip and the M6.4 drop label agree. Affix lines are tinted a consistent "magic" blue. to_html(false) gives the hex without alpha for BBCode. The result is one string the RichTextLabel renders with formatting.

Compare-to-equipped

The most useful addition: alongside the hovered item's stats, show how they differ from what is currently equipped in that slot. The delta is computed by asking "what would my derived stats be with this item instead of the equipped one?" — which is the M5.4 recompute run on a hypothetical modifier list.

Example

A simple per-stat delta: compute the player's key stats with the hovered item swapped in, and show the signed difference.

static func compare_lines(player, hovered: ItemData) -> String:
    var equipped := Equipment.equipped_in(hovered.slot)
    var current := player.modifiers.duplicate()
    var hypothetical := current.duplicate()
    if equipped != null:
        for m in equipped.all_modifiers():
            hypothetical.erase(m)
    hypothetical.append_array(hovered.all_modifiers())
    var now := StatMath.derive(player.base_stats, current)        # M5.4 combine over a list
    var after := StatMath.derive(player.base_stats, hypothetical)
    return _delta_line("Damage", now.damage_max, after.damage_max) \
         + _delta_line("Armor", now.armor, after.armor)

static func _delta_line(label: String, a: int, b: int) -> String:
    var d := b - a
    if d == 0:
        return ""
    var color := "6dff6d" if d > 0 else "ff6d6d"
    return "\n[color=#%s]%s %+d[/color]" % [color, label, d]

It builds the modifier list the player would have (remove the currently-equipped item's modifiers, add the hovered item's), derives stats both ways with the M5.4 combine, and shows the signed delta in green (up) or red (down). The comparison reuses the exact stat math the game uses — no separate "estimate," so the preview matches what actually happens on equip.

Positioning and lifecycle

A tooltip should appear near the cursor without covering the item, clamp to stay on screen, and disappear when the hover ends. _make_custom_tooltip handles show/hide for you; if managing it yourself, position on mouse_entered, free/hide on mouse_exited, and follow the cursor or anchor to the slot.

Walkthrough

  1. Build res://scenes/ui/item_tooltip.tscn: a PanelMarginContainerRichTextLabel (bbcode_enabled on, fit_content on). Attach a small script with a set_item(item, player) that sets the label's text to item_bbcode(item) plus compare_lines(player, item).
  2. Add the affix_line, item_bbcode, and Rarity.tier_name helpers (and a StatMath.derive(base, mods) that runs the M5.4 combine over every stat).
  3. On the bag/equipment slot widget (M7.3), override _make_custom_tooltip(_for_text) to instance the tooltip scene, call set_item with the slot's item and the player, and return it.
  4. Press F5, open the inventory, and hover an item: the tooltip shows its name in rarity color, its tier, and one line per affix. Hover a weapon while another is equipped and confirm the green/red comparison deltas appear and match what equipping actually does.
  5. Tune positioning so the tooltip doesn't cover the hovered slot and stays on screen near the edges.

Optional sanity check

Hover an item, read its comparison deltas, then actually equip it (M7.3 drag) and confirm the player's stats changed by exactly the deltas the tooltip predicted. If they differ, the tooltip's hypothetical is computing stats differently than the real equip path — the two must use the same M5.4 combine. Matching deltas prove the preview and the mechanic share one calculation, which is the whole point of rendering the tooltip from the same data the game applies.

Self-check quiz

Q1 — Why use a custom tooltip (RichTextLabel + BBCode) instead of Godot's built-in tooltip_text?

A. tooltip_text is deprecated. B. The built-in tooltip is plain text; an item tooltip needs rarity-colored name, formatting, and a multi-line affix list (and comparison), which require rich text and a custom Control. C. tooltip_text can't be set at runtime. D. Custom tooltips render faster.

Reveal answer

B. Item tooltips need color and per-line formatting that plain tooltip_text can't provide; a RichTextLabel with BBCode in a custom Control delivers it. A is false. C is false (you can set tooltip_text dynamically — it's just plain). D is not the reason.

Q2 — Why is the affix line computed from the item's StatModifier rather than stored as a description string on the item?

A. Strings use more memory. B. Computing the line from the same modifier the game applies guarantees the text matches the mechanical effect; a separate description string could drift from the actual value. C. Because items can't store strings. D. Because BBCode requires numbers.

Reveal answer

B. Rendering the tooltip from the live StatModifier keeps display and effect as one source of truth — the player reads exactly what the item does. A stored description is a second representation that can disagree with the rolled value (especially since affix values are rolled per drop). A, C, D are false.

Q3 — How does the compare-to-equipped delta avoid being a separate, possibly-wrong estimate of the equip result?

A. It hardcodes typical deltas. B. It builds the modifier list the player would have (remove equipped item's mods, add the hovered item's) and runs the same M5.4 combine used on real equip, so the preview equals the actual outcome. C. It guesses based on rarity. D. It queries the server.

Reveal answer

B. The comparison reuses the exact recompute math: it derives stats for the hypothetical modifier list with the same combine function the real equip uses, so the shown delta is the real delta. A and C are fabricated estimates that would drift. D is irrelevant (single-player, no server).

Integration question

Q4 — open

The tooltip renders the name in the M6.2 rarity color, each affix from its M6.1/M5.4 StatModifier, and a comparison computed with the M5.4 combine. Explain how the tooltip is the final demonstration of the book's single-source-of-truth thesis — that display is always derived from the data the game acts on — and describe what a player sees, end to end, when hovering a rare sword while a common one is equipped.

Reveal expected answer

The tooltip closes the loop on single-source-of-truth: every visible element is derived from the data the game mechanically uses, never from a parallel description. The name's color comes from the same Rarity.COLORS table that colors the M6.4 drop label, so a rare reads yellow everywhere. Each affix line is computed from the item's actual StatModifier (M6.1, the same objects equipping appends in M7.2), so the text the player reads is exactly the bonus the item grants — there is no stored description that could be wrong for a rolled value. The comparison block derives "now" and "after" stats by running the M5.4 combine over the player's current modifier list and a hypothetical list (equipped item's mods removed, hovered item's added), so the predicted delta equals what equipping will actually do. Nothing in the tooltip is an estimate or a duplicate; it is a view of the item data and the stat math. End to end, hovering a rare sword while a common one is equipped: the player sees the sword's name in yellow (rare), its tier, and a line per rolled affix ("+9 Damage", "15% increased Damage") in readable text matching the rolls; below that, a comparison — "Damage +6" in green (the rare beats the common) and perhaps "Armor −2" in red if the common had an armor implicit the rare lacks — computed by the same combine that will run on equip. If the player then equips it, the stats change by exactly those deltas, because the tooltip and the equip share one calculation. The tooltip is the book's thesis made visible: the player's decisions are made on the real numbers, because the display is the data.