Skip to content
This repository was archived by the owner on Sep 20, 2026. It is now read-only.
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
34 changes: 34 additions & 0 deletions src/BazaarPlusPlus/Game/Tooltips/UpgradePreviewValueRegistry.cs
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,12 @@ namespace BazaarPlusPlus.Game.Tooltips;

internal static class UpgradePreviewValueRegistry
{
[ThreadStatic]
private static int _tooltipRenderDepth;

[ThreadStatic]
private static ITooltipComponent? _fallbackComponent;

private static readonly ConditionalWeakTable<
CardTooltipData,
UpgradePreviewValueProjection
Expand All @@ -25,11 +31,39 @@ out float value
)
{
value = default;
component ??= TakeFallbackComponent();
return tooltipData != null
&& Projections.TryGetValue(tooltipData, out var projection)
&& projection.TryResolve(component, out value);
}

internal static void BeginTooltipRender() => _tooltipRenderDepth++;

internal static void EndTooltipRender()
{
if (_tooltipRenderDepth > 0)
_tooltipRenderDepth--;

if (_tooltipRenderDepth == 0)
_fallbackComponent = null;
}

internal static void CaptureFallbackComponent(ITooltipComponent component)
{
// Native RenderTooltip omits the component argument for styled tokens whose
// ReferencedAttribute is null. Resolve() runs immediately before that formatter,
// so retain the token only for the duration of this synchronous render scope.
if (_tooltipRenderDepth > 0)
_fallbackComponent = component;
}

private static ITooltipComponent? TakeFallbackComponent()
{
var component = _fallbackComponent;
_fallbackComponent = null;
return component;
}

internal static bool TryResolveEffectiveCooldowns(
CardTooltipData tooltipData,
out float currentSeconds,
Expand Down
27 changes: 27 additions & 0 deletions src/BazaarPlusPlus/Patches/Tooltips/UpgradePreviewValuePatch.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,40 @@
using System.Reflection;
using BazaarGameClient.Domain.Tooltips;
using BazaarGameShared.Domain.Core.Types;
using BazaarGameShared.Domain.Effect.AuraActions;
using BazaarPlusPlus.Game.Tooltips;
using HarmonyLib;
using TheBazaar;
using TheBazaar.Tooltips;

namespace BazaarPlusPlus.Patches.Tooltips;

[HarmonyPatch(typeof(CardTooltipData), "RenderTooltip", [typeof(TooltipBuilder)])]
internal static class UpgradePreviewTooltipRenderScopePatch
{
[HarmonyPrefix]
private static void Prefix() => UpgradePreviewValueRegistry.BeginTooltipRender();

[HarmonyFinalizer]
private static void Finalizer() => UpgradePreviewValueRegistry.EndTooltipRender();
}

[HarmonyPatch(typeof(TooltipComponentAura), nameof(TooltipComponentAura.Resolve))]
internal static class UpgradePreviewAuraValueContextPatch
{
[HarmonyPostfix]
private static void Postfix(TooltipComponentAura __instance)
{
if (
!__instance.ReferencedAttribute.HasValue
&& __instance.Aura.Action
is TAuraActionCardModifyAttribute
or TAuraActionPlayerModifyAttribute
)
UpgradePreviewValueRegistry.CaptureFallbackComponent(__instance);
}
}

[HarmonyPatch]
internal static class UpgradePreviewValuePatch
{
Expand Down
95 changes: 95 additions & 0 deletions tests/UpgradePreviewTooltip.Tests/Program.cs
Original file line number Diff line number Diff line change
Expand Up @@ -104,6 +104,101 @@ out var projection
);
AssertFloatEqual(3f, upgradedBurn, "Derived Burn should be recomputed as 20% of 15, not 2 + 5.");

var guzhengAura = new TCardAura
{
Id = "guzheng-damage-per-tempo",
Action = new TAuraActionCardModifyAttribute
{
AttributeType = ECardAttributeType.DamageAmount,
Operation = EAttributeModifierOperation.Add,
Target = new TTargetCardSelf(),
Value = new TReferenceValuePlayerAttribute
{
AttributeType = EPlayerAttributeType.Tempo,
Modifier = new TValueModifier
{
ModifyMode = EValueModifierMode.Multiply,
Value = new TReferenceValueCardAttribute
{
AttributeType = ECardAttributeType.Custom_0,
Target = new TTargetCardSelf(),
Modifier = new TValueModifier
{
ModifyMode = EValueModifierMode.Multiply,
Value = new TFixedValue { Value = 1f },
},
},
},
},
},
};
var guzhengTemplate = new TCardItem
{
Type = ECardType.Item,
StartingTier = ETier.Gold,
Auras = new Dictionary<string, TCardAura> { [guzhengAura.Id] = guzhengAura },
Tiers = new Dictionary<ETier, TCardTier>
{
[ETier.Gold] = new TCardTier
{
Attributes = new Dictionary<ECardAttributeType, int>
{
[ECardAttributeType.Custom_0] = 15,
},
},
[ETier.Diamond] = new TCardTier
{
Attributes = new Dictionary<ECardAttributeType, int>
{
[ECardAttributeType.Custom_0] = 30,
},
},
},
};
var guzhengCard = new ItemCard
{
Template = guzhengTemplate,
Type = ECardType.Item,
Tier = ETier.Gold,
Attributes = new Dictionary<ECardAttributeType, int> { [ECardAttributeType.Custom_0] = 15 },
};
var guzhengContext = new TooltipContext(
guzhengCard,
guzhengTemplate,
new ValueContext(null!, guzhengCard)
);
var guzhengToken = TooltipComponentAura.Create(
guzhengContext,
guzhengAura.Id,
ETooltipAccessorType.Mod,
0
);

AssertNotNull(guzhengToken, "Guzheng aura token should be created.");
AssertTrue(
!guzhengToken!.ReferencedAttribute.HasValue,
"Guzheng's player-attribute aura intentionally has no native referenced card attribute."
);
AssertFloatEqual(15f, guzhengToken.Resolve()!.Value, "Current Guzheng multiplier should be 15.");
AssertTrue(
UpgradePreviewValueProjection.TryCreate(
guzhengCard,
guzhengTemplate,
guzhengContext.ValueContext,
out var guzhengProjection
),
"Guzheng upgrade projection should be created."
);
AssertTrue(
guzhengProjection.TryResolve(guzhengToken, out var upgradedGuzhengMultiplier),
"Projected Guzheng aura token should resolve."
);
AssertFloatEqual(
30f,
upgradedGuzhengMultiplier,
"Guzheng Damage and Shield per Tempo should upgrade from 15 to 30."
);

Comment on lines +192 to +201
card.Attributes[ECardAttributeType.Custom_1] = 12;
AssertTrue(
UpgradePreviewValueProjection.TryCreate(
Expand Down