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
289 changes: 289 additions & 0 deletions docs/drafts/2026-07-30-recap-right-click-not-received.md

Large diffs are not rendered by default.

Original file line number Diff line number Diff line change
Expand Up @@ -211,7 +211,8 @@ string nativeAttributeKey
&& !(
surface == CombatImpactEventSurface.AppliedEffect
&& nativeAttributeKey
is "TempoRemoveAmount"
is "RegenApplyAmount"
or "TempoRemoveAmount"
or "BurnRemoveAmount"
or "PoisonRemoveAmount"
or "RegenRemoveAmount"
Expand Down
111 changes: 106 additions & 5 deletions src/BazaarPlusPlus/Game/PostCombatImpact/PostCombatImpactController.cs
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ internal sealed class PostCombatImpactController : MonoBehaviour
private const int PositionedGeometrySettleMaxFrames = 30;
private const int RequiredStableGeometrySamples = 3;
private const float GeometryEpsilon = 0.5f;
private const int MaxTransientShowRetries = 2;

private readonly WaitForEndOfFrame _waitForEndOfFrame = new();
private PostCombatImpactModule? _module;
Expand All @@ -38,6 +39,8 @@ internal sealed class PostCombatImpactController : MonoBehaviour
private string? _selectedSourceId;
private int _hoverRevision;
private int _suppressedHoverRevision = -1;
private int _transientShowRetryRevision = -1;
private int _transientShowRetries;
private bool _nativeAuxiliaryTakeoverActive;
private CombatImpactPerspective _requestedPerspective = CombatImpactPerspective.Caused;

Expand Down Expand Up @@ -165,8 +168,18 @@ Vector3 tooltipOffset
);
CancelAndClearPresentation(preserveOutstandingAuxiliaryRequest: true);
if (!sameHover)
LogInteraction(PostCombatImpactReasonCode.RecapHoverObserved);
LogInteractionTrace(
PostCombatImpactReasonCode.RecapHoverObserved,
card.TemplateId,
detail: "hover"
);
StartPendingShow();
if (_pendingShow == null && _settlePresentation == null)
LogInteractionTrace(
PostCombatImpactReasonCode.PendingShowBlocked,
card.TemplateId,
DescribePendingShowBlock()
);
TryPrepareCurrentPrimaryTooltip(card);
}

Expand Down Expand Up @@ -205,10 +218,15 @@ or PostCombatImpactHoverExitOrigin.SkillPointerExit
);
return;
}
var dismissedTemplateId = _hoveredRequest.Card.TemplateId;
ClearHoveredSource();
CancelAndClearPresentation(preserveOutstandingAuxiliaryRequest: true);
if (hadWork)
LogInteraction(PostCombatImpactReasonCode.Dismissed);
LogInteractionTrace(
PostCombatImpactReasonCode.Dismissed,
dismissedTemplateId,
detail: "pointer_exit"
);
}

private System.Collections.IEnumerator DismissAfterLockedPointerExit(
Expand All @@ -226,10 +244,15 @@ bool hadWork
)
yield break;

var dismissedTemplateId = _hoveredRequest.Card.TemplateId;
ClearHoveredSource();
CancelAndClearPresentation(preserveOutstandingAuxiliaryRequest: true);
if (hadWork)
LogInteraction(PostCombatImpactReasonCode.Dismissed);
LogInteractionTrace(
PostCombatImpactReasonCode.Dismissed,
dismissedTemplateId,
detail: "locked_pointer_exit"
);
}

private void CancelPendingHoverExit()
Expand Down Expand Up @@ -262,6 +285,11 @@ private System.Collections.IEnumerator ShowWhenReady(HoverRequest request, int r

if (!IsCurrentHover(request, revision) || !IsRecapOpen())
{
LogInteractionTrace(
PostCombatImpactReasonCode.PendingShowAborted,
request.Card.TemplateId,
IsRecapOpen() ? "hover_changed" : "recap_closed"
);
_pendingShow = null;
yield break;
}
Expand Down Expand Up @@ -481,6 +509,28 @@ private System.Collections.IEnumerator ShowWhenReady(HoverRequest request, int r

if (!shown)
{
if (
IsCurrentHover(request, revision)
&& IsRecapOpen()
&& TryConsumeTransientShowRetry(revision)
)
{
// TryOpen fails soft when the native controller's Destroy is already pending — a
// state no managed liveness check can see before frame end. Discard the dying
// auxiliary and re-request a fresh one instead of suppressing the still-valid
// hover.
var deadAuxiliary = _pendingAuxiliaryController;
if (deadAuxiliary != null)
{
_view.CancelPreparedNativeAuxiliary(deadAuxiliary);
_pendingAuxiliaryController = null;
tooltipParent.HideAuxiliaryTooltipController();
}
_pendingShow = null;
LogInteraction(PostCombatImpactReasonCode.AuxiliaryTooltipShowRetried);
StartPendingShow();
yield break;
}
FailPendingShow(
revision,
PostCombatImpactReasonCode.AuxiliaryTooltipContentUnavailable,
Expand Down Expand Up @@ -646,10 +696,12 @@ bool hasAttributedImpact
}

_settlePresentation = null;
LogInteraction(
LogInteractionTrace(
!hasAttributedImpact
? PostCombatImpactReasonCode.ShownWithoutAttributedImpact
: PostCombatImpactReasonCode.Shown
: PostCombatImpactReasonCode.Shown,
request.Card.TemplateId,
detail: "settled"
);
}

Expand Down Expand Up @@ -769,6 +821,17 @@ string header
return;
}

// A show carrying this feature's own header that fails the request match is the orphan
// hazard: the prepared concealment was just handed back and nothing re-conceals it.
if (_view != null && string.Equals(header, _view.Header, StringComparison.Ordinal))
LogInteractionTrace(
PostCombatImpactReasonCode.NativeAuxiliaryUnmatched,
_hoveredRequest?.Card.TemplateId ?? Guid.Empty,
!_auxiliaryRequestOutstanding ? "no_outstanding_request"
: !ReferenceEquals(_pendingAuxiliaryAnchor, anchor) ? "anchor_mismatch"
: "header_mismatch"
);

if (_pendingShow != null || _settlePresentation != null)
{
StopPendingShow(hidePrimary: true, preserveOutstandingAuxiliaryRequest: true);
Expand Down Expand Up @@ -872,6 +935,29 @@ private void ClearHoveredSource()
private bool IsCurrentHover(HoverRequest request, int revision) =>
revision == _hoverRevision && ReferenceEquals(_hoveredRequest, request);

private string DescribePendingShowBlock() =>
_hoveredRequest == null ? "no_hover"
: _suppressedHoverRevision == _hoverRevision ? "suppressed"
: _auxiliaryRequestOutstanding ? "outstanding_request"
: "unknown";

/// <summary>
/// Grants a bounded number of same-hover re-requests after a transient show failure, so a
/// persistent failure still ends in suppression instead of a hide/show loop.
/// </summary>
private bool TryConsumeTransientShowRetry(int revision)
{
if (_transientShowRetryRevision != revision)
{
_transientShowRetryRevision = revision;
_transientShowRetries = 0;
}
if (_transientShowRetries >= MaxTransientShowRetries)
return false;
_transientShowRetries++;
return true;
}

private void TryPrepareCurrentPrimaryTooltip(Card card)
{
if (
Expand Down Expand Up @@ -1036,6 +1122,21 @@ private static void LogInteraction(PostCombatImpactReasonCode reasonCode) =>
() => [PostCombatImpactLogEvents.ReasonCode.Bind(reasonCode)]
);

private static void LogInteractionTrace(
PostCombatImpactReasonCode reasonCode,
Guid cardTemplateId,
string detail
) =>
BppLog.DebugEvent(
PostCombatImpactLogEvents.InteractionTraced,
() =>
[
PostCombatImpactLogEvents.ReasonCode.Bind(reasonCode),
PostCombatImpactLogEvents.Card.Bind(cardTemplateId),
PostCombatImpactLogEvents.Detail.Bind(detail),
]
);

private static void LogInteractionFailure(
PostCombatImpactReasonCode reasonCode,
Exception exception
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,14 +14,23 @@ internal enum PostCombatImpactReasonCode
PrimaryTooltipCreateTimedOut,
AuxiliaryTooltipCreateTimedOut,
AuxiliaryTooltipContentUnavailable,
AuxiliaryTooltipShowRetried,
AuxiliaryTooltipPositionUnavailable,
TypographyUnavailable,
PairOpenMissingAuxiliaryFields,
PairOpenDyingController,
PairOpenMissingBackground,
PairOpenBackgroundCloneRejected,
NativeAuxiliaryDisplaced,
NativeAuxiliaryHidden,
NativeAuxiliaryRequeued,
TooltipRenderException,
Dismissed,
RecapHoverObserved,
StaleRequestDiscarded,
PendingShowBlocked,
PendingShowAborted,
NativeAuxiliaryUnmatched,
PairPlacementOverflowed,
PairPlacementTooNarrow,
PairTopAlignmentAdjusted,
Expand Down Expand Up @@ -64,6 +73,33 @@ internal static class PostCombatImpactLogEvents
[ReasonCode]
);

internal static readonly BppLogFieldDefinition Card = new(
1,
"card",
BppLogFieldPrivacy.Public,
BppLogCorrelationPolicy.None,
BppLogCardinality.High
);

internal static readonly BppLogFieldDefinition Detail = new(
2,
"detail",
BppLogFieldPrivacy.Public,
BppLogCorrelationPolicy.None,
BppLogCardinality.Low
);

/// <summary>
/// Card-identified interaction trail. Exists because the plain observed trail proved unable
/// to attribute silent per-card failures: it shows a hover with no outcome but not which card
/// or which silent gate swallowed it.
/// </summary>
internal static readonly BppLogEventDefinition InteractionTraced = new(
BppLogFeatureScope.PostCombatImpact,
"post_combat_impact.interaction.traced",
[ReasonCode, Card, Detail]
);

internal static readonly BppLogEventDefinition InteractionDegraded = new(
BppLogFeatureScope.PostCombatImpact,
"post_combat_impact.interaction.degraded",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,27 @@ internal enum PairSide
Left,
}

/// <summary>
/// Why the session's TryOpen reported the native pair as unusable. The host stays log-free; the
/// consuming feature owns turning these into its own reason codes.
/// </summary>
internal enum NativePairedTooltipOpenFailure
{
None,

/// <summary>The auxiliary controller is missing auxParent/header/body fields.</summary>
MissingAuxiliaryFields,

/// <summary>Gate creation failed — the controller's Destroy is already pending.</summary>
DyingController,

/// <summary>The auxiliary or primary background image/sprite is unavailable.</summary>
MissingBackground,

/// <summary>The native background clone could not be built from the primary.</summary>
BackgroundCloneRejected,
}

/// <summary>
/// Shared tolerance for the paired-tooltip host.
/// </summary>
Expand Down
Loading