Concept: Style Resolution, Motion & Rendering

Once an event survives the per-player filter, four client-side systems turn it into pixels: the registry picks how it looks, the motion set picks how it moves, the formatter picks how the number reads, and the renderer decides how it is drawn.

Style resolution

Resolution runs in six ordered steps. You can watch all of them live in the Dev Tools Resolver tab, which is the fastest way to tell "my style is wrong" from "my tags are wrong".

  1. Advanced rules - only if AdvancedRules is non-empty. A matching FGameplayTagQuery with the highest Priority wins outright.
  2. Hierarchical tag match - each incoming tag walks up its parents until a Styles key hits. The most specific match across the whole container wins.
  3. Fallback - DefaultStyle when nothing matched.
  4. Modifiers - every Modifiers key present in the container is applied on top, in ApplyOrder. A modifier may override the motion preset, override the placement preset, and add to the placement offset.
  5. Motion - MotionPreset resolves against the MotionSet, falling back to the style's inline Motion; the accumulated DurationMultiplier folds into Duration.
  6. Placement - PlacementPreset resolves against the PlacementSet, and the style's plus every modifier's PlacementOffset accumulate. No placement is legal and common: it means the pop anchors over its target.

Hierarchy is what keeps the registry small. A hit tagged Damage.Fire.Burn resolves against Damage.Fire if that row exists, otherwise Damage, otherwise DefaultStyle - so you author only the specificity you actually care about.

Modifiers stop the combinatorial explosion
Modifiers are the reason you do not need a row per combination. One Crit modifier covers Damage.Fire.Crit, Damage.Ice.Crit and every other pairing. Without them a registry grows as styles x conditions; with them it grows as styles + conditions.
Placement is resolved, not simulated
Steps 1-6 pick a placement OBJECT; they never compute a position. Position happens later, per frame, when the subsystem asks that placement where the pop is now. That split is what lets one shared, stateless placement serve every pop routed to it - see How-To: Write Your Own Placement.

Format tokens

The resolved style supplies a rich-text FormatString with these tokens:

TokenReplaced with
{Value}The formatted number
{Prefix}The event's PrefixText (e.g. - or +)
{Custom}The event's CustomText (e.g. Blocked!)
{ValueStyle} / {PrefixStyle}The style's rich-text row names - wrap segments as <{ValueStyle}>{Value}</>

A modifier's FormatWrapper wraps whatever the style produced, with {0} standing in for the base result - so "{0}!" turns 123 into 123!. Stacked wrappers nest outward in ApplyOrder.

Number formatting

The project default (UCrimsonCombatTextSettings::NumberFormat) applies unless the local player's NumberFormatOverride is 0 or greater:

FormatExampleNotes
CommaSeparated1,234,567Locale-style grouping, no decimals
Abbreviated1.2M, 3.4K, 5.6B, 7.8TCompact suffix notation with one decimal
Raw1234567No formatting

Motion

Motion is authored as curves over a normalized 0..1 lifetime alpha, not as velocity and gravity. The evaluated sample is a pure function of that alpha, which has two consequences worth understanding:

  • Motion always completes exactly at `Duration`. The old failure mode - an arc cut off mid-fall because gravity and duration disagreed - cannot occur.
  • Motion is reproducible. The same fan slot replays identically, so repeated attacks read consistently instead of scattering randomly.

Amplitudes are in layout units (post-DPI), so motion looks the same at 1080p and 4K. An empty curve falls back to a documented default rather than to zero, so a default-constructed motion still produces a sensible rise and fade.

Simulation and rendering

The subsystem owns every live instance and simulates them in one loop: advance time, ask each pop's placement where it is now, project to screen, evaluate motion, retire the expired. It then hands the whole frame to a renderer. Widgets are passive views - they carry no motion, no timing and no lifetime, and are marked DisableNativeTick.

DisableNativeTick is not TickFrequency Never
DisableNativeTick is deliberately not the same as setting a widget's TickFrequency to Never. Never would also stop UWidgetAnimation and latent actions, silently breaking the AnimationName feature. The metadata flag drops only the native tick.

Views are pooled and recycled, never destroyed mid-session. MaxActiveTexts caps concurrent instances and retires the oldest first, which is what keeps an AoE spike from unbounded widget growth. Creation is additionally throttled per frame when the pool is exhausted.