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
3 changes: 2 additions & 1 deletion docs/adr/0008-combat-impact-accounting-contract.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,8 @@ Combat Impact used to place effect applications, activation observations, amount

Every displayed number carries dimension, basis, coverage, and provenance in the `CombatImpactEvent` ledger.

- Additive breakdowns require the same dimension and basis. An incomplete breakdown renders `attributed N / total M` with every non-zero remainder bucket; zero attribution renders the total plus `breakdown unavailable`.
- Additive breakdowns require the same dimension and basis; every non-zero remainder bucket stays in the internal ledger. Attribution gaps and accounting diagnostics are not player-facing tooltip copy.
- Native application-count controls stay internal, even when comparable. The tooltip shows the recorded effect count and gameplay amount or duration without appending a second count such as `cards affected` or `applications`. Native damage, healing, and shield amounts remain gameplay totals to display ([formatter](../../src/BazaarPlusPlus/Game/PostCombatImpact/Data/CombatImpactMetricFormatter.cs)).
- Conservation is per view. The received view intentionally omits hero/player targets, so no caused-versus-received grand total exists ([tests](../../tests/PostCombatImpact.Tests/CombatImpactAggregatorTests.cs)).
- `ObservedActivationBatchCount` is a distinct trigger-source/frame observation, never an exact trigger count or an application count ([models](../../src/BazaarPlusPlus/Game/PostCombatImpact/Data/CombatImpactModels.cs)).
- Producer-assigned `CombatImpactTriggerScope` distinguishes external, self, trigger-fallback, no-evidence, unattributed, and not-applicable provenance. Rejected trigger, target, source, and value evidence survives as typed residuals.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -78,36 +78,17 @@ internal static string Group(
: $"{effectMarker}{value}"
);
}
else
else if (group.ObservedValue.HasValue && !group.HasUnattributedTransitionValue)
{
if (group.ObservedValue.HasValue && !group.HasUnattributedTransitionValue)
{
parts.Add(
ObservedValue(
group.Kind,
group.ObservedValue.Value,
group.Unit,
chinese,
effectMarker
)
);
}

if (
authoritative?.Basis == CombatImpactAuthoritativeBasis.ApplicationCount
&& (group.Count == 0 || authoritative.Value != group.Count)
)
{
parts.Add(
authoritative.CanReconcileApplicationCount
? chinese
? $"生效 {authoritative.Value} 次"
: $"{authoritative.Value} application{(authoritative.Value == 1 ? string.Empty : "s")}"
: chinese
? $"影响 {authoritative.Value} 张卡牌"
: $"{authoritative.Value} card{(authoritative.Value == 1 ? string.Empty : "s")} affected"
);
}
parts.Add(
ObservedValue(
group.Kind,
group.ObservedValue.Value,
group.Unit,
chinese,
effectMarker
)
);
}

return string.Join(" · ", parts);
Expand Down
33 changes: 16 additions & 17 deletions tests/PostCombatImpact.Tests/CombatImpactMetricFormatterTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -638,7 +638,7 @@ public void Target_breakdown_distinguishes_counts_from_authoritative_totals()
}

[Fact]
public void Authoritative_bases_do_not_retype_observed_duration_or_invent_counts()
public void Native_count_controls_stay_internal_while_gameplay_totals_remain_visible()
{
var applications = new CombatImpactAuthoritativeMetric(
CombatImpactKind.Haste,
Expand Down Expand Up @@ -677,13 +677,14 @@ public void Authoritative_bases_do_not_retype_observed_duration_or_invent_counts
canReconcileApplicationCount: true
),
};
var affectedCards = matching with
var nonComparableCount = matching with
{
Count = 7,
Count = 23,
ObservedValue = 69000,
AuthoritativeMetric = new CombatImpactAuthoritativeMetric(
CombatImpactKind.Haste,
"HasteAmount",
10,
24,
CombatImpactValueUnit.Applications,
CombatImpactAuthoritativeBasis.ApplicationCount
),
Expand Down Expand Up @@ -722,27 +723,25 @@ public void Authoritative_bases_do_not_retype_observed_duration_or_invent_counts
};

Assert.Equal("×10 · 9.50s", CombatImpactMetricFormatter.Group(matching, chinese: false));
Assert.Equal(
"×7 · 9.50s · 10 applications",
CombatImpactMetricFormatter.Group(divergent, chinese: false)
);
Assert.Equal(
"×7 · 9.50s · 生效 10 次",
CombatImpactMetricFormatter.Group(divergent, chinese: true)
);
Assert.Equal("×7 · 9.50s", CombatImpactMetricFormatter.Group(divergent, chinese: false));
Assert.Equal("×7 · 9.50s", CombatImpactMetricFormatter.Group(divergent, chinese: true));
Assert.Equal("×10 · 9.50s", CombatImpactMetricFormatter.Group(estimated, chinese: false));
Assert.Equal("×10 · 9.50s", CombatImpactMetricFormatter.Group(estimated, chinese: true));
Assert.Equal(
"9.50s · 1 application",
"9.50s",
CombatImpactMetricFormatter.Group(singularApplication, chinese: false)
);
Assert.Equal(
"×7 · 9.50s · 10 cards affected",
CombatImpactMetricFormatter.Group(affectedCards, chinese: false)
"9.50s",
CombatImpactMetricFormatter.Group(singularApplication, chinese: true)
);
Assert.Equal(
"×23 · 69s",
CombatImpactMetricFormatter.Group(nonComparableCount, chinese: false)
);
Assert.Equal(
"×7 · 9.50s · 影响 10 张卡牌",
CombatImpactMetricFormatter.Group(affectedCards, chinese: true)
"×23 · 69s",
CombatImpactMetricFormatter.Group(nonComparableCount, chinese: true)
);
Assert.Equal(
"76 total",
Expand Down