diff --git a/docs/adr/0008-combat-impact-accounting-contract.md b/docs/adr/0008-combat-impact-accounting-contract.md index cec21f34..bf8c0078 100644 --- a/docs/adr/0008-combat-impact-accounting-contract.md +++ b/docs/adr/0008-combat-impact-accounting-contract.md @@ -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. diff --git a/src/BazaarPlusPlus/Game/PostCombatImpact/Data/CombatImpactMetricFormatter.cs b/src/BazaarPlusPlus/Game/PostCombatImpact/Data/CombatImpactMetricFormatter.cs index 3592ecc3..149897ad 100644 --- a/src/BazaarPlusPlus/Game/PostCombatImpact/Data/CombatImpactMetricFormatter.cs +++ b/src/BazaarPlusPlus/Game/PostCombatImpact/Data/CombatImpactMetricFormatter.cs @@ -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); diff --git a/tests/PostCombatImpact.Tests/CombatImpactMetricFormatterTests.cs b/tests/PostCombatImpact.Tests/CombatImpactMetricFormatterTests.cs index 0b31d6fc..bd54b68f 100644 --- a/tests/PostCombatImpact.Tests/CombatImpactMetricFormatterTests.cs +++ b/tests/PostCombatImpact.Tests/CombatImpactMetricFormatterTests.cs @@ -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, @@ -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 ), @@ -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",