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".
- Advanced rules - only if
AdvancedRulesis non-empty. A matchingFGameplayTagQuerywith the highestPrioritywins outright. - Hierarchical tag match - each incoming tag walks up its parents until a
Styleskey hits. The most specific match across the whole container wins. - Fallback -
DefaultStylewhen nothing matched. - Modifiers - every
Modifierskey present in the container is applied on top, inApplyOrder. A modifier may override the motion preset, override the placement preset, and add to the placement offset. - Motion -
MotionPresetresolves against theMotionSet, falling back to the style's inlineMotion; the accumulatedDurationMultiplierfolds intoDuration. - Placement -
PlacementPresetresolves against thePlacementSet, and the style's plus every modifier'sPlacementOffsetaccumulate. 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.
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.Format tokens
The resolved style supplies a rich-text FormatString with these tokens:
| Token | Replaced 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:
| Format | Example | Notes |
|---|---|---|
CommaSeparated | 1,234,567 | Locale-style grouping, no decimals |
Abbreviated | 1.2M, 3.4K, 5.6B, 7.8T | Compact suffix notation with one decimal |
Raw | 1234567 | No 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 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.