Localization Formatters - Slay The Spire 2 Research Note
Originally from my ProjectSpire repo which is the monorepo hosting everything Slay The Spire 2 related things.
Reposting it here because I am AMAZED amazed amazed 🪨🪨🪨 by how well coding agents can do these research in minutes and give you a detailed documentation.
Initial notes from decompiled
v0.103.2sources.
This document records how card localization formatter functions such as diff() are resolved and applied.
Purpose
Card localization strings can contain SmartFormat expressions such as:
"ABRASIVE.description": "Gain {DexterityPower:diff()} [gold]Dexterity[/gold].\nGain {ThornsPower:diff()} [gold]Thorns[/gold]."
The diff() part is not a method on the card class and is not defined in the JSON. It is a SmartFormat formatter registered by the game's localization manager.
Formatter Registration
LocManager.LoadLocFormatters() creates the game's SmartFormatter and registers several custom formatters:
_smartFormatter.AddExtensions(
listFormatter,
new PluralLocalizationFormatter(),
new ConditionalFormatter(),
new ChooseFormatter(),
new SubStringFormatter(),
new IsMatchFormatter(),
new LocaleNumberFormatter(),
new DefaultFormatter(),
new AbsoluteValueFormatter(),
new EnergyIconsFormatter(),
new StarIconsFormatter(),
new HighlightDifferencesFormatter(),
new HighlightDifferencesInverseFormatter(),
new PercentMoreFormatter(),
new PercentLessFormatter(),
new ShowIfUpgradedFormatter());
Relevant source:
Lab/decompiled/v0.103.2/MegaCrit.Sts2.Core.Localization/LocManager.cs
diff() Formatter
diff() is provided by HighlightDifferencesFormatter.
The formatter advertises the SmartFormat name diff:
public string Name
{
get
{
return "diff";
}
set
{
throw new NotImplementedException();
}
}
It only handles values that are DynamicVar instances:
public bool TryEvaluateFormat(IFormattingInfo formattingInfo)
{
if (!(formattingInfo.CurrentValue is DynamicVar dynamicVar))
{
return false;
}
formattingInfo.Write(dynamicVar.ToHighlightedString(inverse: false));
return true;
}
Relevant source:
Lab/decompiled/v0.103.2/MegaCrit.Sts2.Core.Localization.Formatters/HighlightDifferencesFormatter.cs
There is also an inverse version named inverseDiff, implemented by HighlightDifferencesInverseFormatter, which calls ToHighlightedString(inverse: true).
Dynamic Variable Highlighting
DynamicVar.ToHighlightedString() compares the current preview value against the enchanted value, unless the variable was just upgraded:
public string ToHighlightedString(bool inverse)
{
int value = (int)PreviewValue;
int value2 = (int)EnchantedValue;
return StsTextUtilities.HighlightChangeText(
baseComparison: WasJustUpgraded ? 1 : ((!inverse) ? value.CompareTo(value2) : value2.CompareTo(value)),
text: value.ToString(CultureInfo.InvariantCulture));
}
The highlighting itself is handled by StsTextUtilities.HighlightChangeText():
public static string HighlightChangeText(string text, int baseComparison)
{
StringBuilder stringBuilder = new StringBuilder(text);
if (baseComparison == 0)
{
return stringBuilder.ToString();
}
string text2 = ((baseComparison > 0) ? "green" : "red");
stringBuilder.Insert(0, "[" + text2 + "]");
stringBuilder.Append("[/" + text2 + "]");
return stringBuilder.ToString();
}
Therefore:
- comparison
0renders plain text - comparison
> 0wraps the value in[green]...[/green] - comparison
< 0wraps the value in[red]...[/red] WasJustUpgraded == trueforces green highlighting
Relevant sources:
Lab/decompiled/v0.103.2/MegaCrit.Sts2.Core.Localization.DynamicVars/DynamicVar.csLab/decompiled/v0.103.2/MegaCrit.Sts2.Core.TextEffects/StsTextUtilities.cs
Card Description Code Path
Card descriptions are formatted by CardModel.GetDescriptionForPile().
The method:
- Creates the card description
LocString. - Adds all card dynamic variables to that
LocString. - Adds extra formatting variables such as upgrade state, combat state, target state, and icon paths.
- Calls
description.GetFormattedText().
Relevant excerpt:
LocString description = Description;
DynamicVars.AddTo(description);
AddExtraArgsToDescription(description);
...
span[index] = description.GetFormattedText();
LocString.GetFormattedText() delegates to:
return LocManager.Instance.SmartFormat(this, _variables);
Relevant sources:
Lab/decompiled/v0.103.2/MegaCrit.Sts2.Core.Models/CardModel.csLab/decompiled/v0.103.2/MegaCrit.Sts2.Core.Localization/LocString.cs
Worked Example: Abrasive
The Abrasive card defines two canonical dynamic variables:
protected override IEnumerable<DynamicVar> CanonicalVars => new global::_003C_003Ez__ReadOnlyArray<DynamicVar>(new DynamicVar[2]
{
new PowerVar<ThornsPower>(4m),
new PowerVar<DexterityPower>(1m)
});
PowerVar<T> names itself with typeof(T).Name, so these variables are named:
ThornsPowerDexterityPower
Relevant sources:
Lab/decompiled/v0.103.2/MegaCrit.Sts2.Core.Models.Cards/Abrasive.csLab/decompiled/v0.103.2/MegaCrit.Sts2.Core.Localization.DynamicVars/PowerVar.cs
The raw localization string references those variable names:
"ABRASIVE.description": "Gain {DexterityPower:diff()} [gold]Dexterity[/gold].\nGain {ThornsPower:diff()} [gold]Thorns[/gold]."
Normal display
Before upgrade preview or combat modifiers:
| Variable | BaseValue | EnchantedValue | PreviewValue | WasJustUpgraded |
|---|---|---|---|---|
DexterityPower |
1 | 1 | 1 | false |
ThornsPower |
4 | 4 | 4 | false |
Both diff() comparisons are 0, so both values render without color:
Gain 1 [gold]Dexterity[/gold].
Gain 4 [gold]Thorns[/gold].
Upgrade preview
Abrasive.OnUpgrade() upgrades only ThornsPower:
protected override void OnUpgrade()
{
base.DynamicVars["ThornsPower"].UpgradeValueBy(2m);
}
For the upgrade preview:
| Variable | BaseValue | EnchantedValue | PreviewValue | WasJustUpgraded |
|---|---|---|---|---|
DexterityPower |
1 | 1 | 1 | false |
ThornsPower |
6 | 6 | 6 | true |
DexterityPower:diff() still renders plain 1.
ThornsPower:diff() calls ToHighlightedString(false). Because WasJustUpgraded is true, the formatter forces a positive comparison and renders the value green:
Gain 1 [gold]Dexterity[/gold].
Gain [green]6[/green] [gold]Thorns[/gold].
Combat Preview Notes
diff() is not only for upgrade previews.
PowerVar<T>.UpdateCardPreview() can update PreviewValue through global hooks:
base.PreviewValue = Hook.ModifyPowerAmountGiven(
card.CombatState,
ModelDb.Power<T>(),
card.Owner.Creature,
base.BaseValue,
target,
card,
out IEnumerable<AbstractModel> _);
CardModel.UpdateDynamicVarPreview() calls UpdateCardPreview() for each dynamic variable when the card is in an applicable preview context.
This means diff() can also highlight live combat-modified values, not just upgraded values.