diff --git a/CounterStrikeProgression.Core/SkillShots/SkillShotCatalog.cs b/CounterStrikeProgression.Core/SkillShots/SkillShotCatalog.cs
index 5b24d1c..3c7c5af 100644
--- a/CounterStrikeProgression.Core/SkillShots/SkillShotCatalog.cs
+++ b/CounterStrikeProgression.Core/SkillShots/SkillShotCatalog.cs
@@ -97,7 +97,7 @@ internal static class SkillShotCatalog
Kill("untouchable", "Untouchable", "Three kills in one life without taking damage", Rarity.Rare, Any, (e, _) => e.KillsUnhurt == 3),
Kill("killstreak_10", "10-kill streak", "Ten kills in one life", Rarity.Rare, Any, (e, _) => e.LifeKills == 10),
Kill("killstreak_20", "20-kill streak", "Twenty kills in one life", Rarity.Epic, Any, (e, _) => e.LifeKills == 20),
- Kill("collateral", "Collateral", "Kill two players with one bullet", Rarity.Epic, Gun, (e, _) => e.KillsWithThisBullet >= 2),
+ Kill("collateral", "Collateral", "Kill two players with one bullet", Rarity.Epic, GunNotShotgun, (e, _) => e.KillsWithThisBullet >= 2),
Kill("opening_kill", "Opening kill", "Get the first kill of the round", Rarity.Common, Any, (e, _) => e.IsOpeningKill),
Kill("trade", "Trade", "Kill a teammate's killer right away", Rarity.Common, Any, (e, _) => e.IsTrade),
Kill("revenge", "Revenge", "Kill a player who was dominating you", Rarity.Common, Any, (e, _) => e.IsRevenge),
diff --git a/CounterStrikeProgression.Core/Tracking/RoundTracker.cs b/CounterStrikeProgression.Core/Tracking/RoundTracker.cs
index 66ff7ba..923c9e7 100644
--- a/CounterStrikeProgression.Core/Tracking/RoundTracker.cs
+++ b/CounterStrikeProgression.Core/Tracking/RoundTracker.cs
@@ -9,7 +9,7 @@ namespace ktsu.CounterStrikeProgression.Core.Tracking;
///
/// Whether it's the round's first kill.
/// The attacker's kills in the current chain, counting this one.
-/// The attacker's kills on this tick, counting this one.
+/// The attacker's kills with this weapon on this tick, counting this one.
/// Whether the victim had just killed one of the attacker's teammates.
internal readonly record struct KillContext(bool IsOpeningKill, int ChainedKills, int KillsWithThisBullet, bool IsTrade);
@@ -48,9 +48,13 @@ public void StartRound()
/// The victim's id.
/// The victim's side.
/// The game time in seconds.
- /// The server tick, which identifies kills made with one bullet.
+ /// The server tick, which with the weapon identifies kills made with one bullet.
+ ///
+ /// The weapon's key. Kills on one tick count as one bullet only when they're with the same weapon, so a molotov
+ /// finishing one enemy as a rifle kills another isn't a collateral.
+ ///
/// The round context for the kill.
- public KillContext RegisterKill(int attacker, int attackerTeam, int victim, int victimTeam, double time, int tick)
+ public KillContext RegisterKill(int attacker, int attackerTeam, int victim, int victimTeam, double time, int tick, string? weapon = null)
{
bool opening = !anyKill;
anyKill = true;
@@ -63,8 +67,9 @@ public KillContext RegisterKill(int attacker, int attackerTeam, int victim, int
chain.Count++;
chain.LastKillTime = time;
- chain.KillsThisTick = chain.LastTick == tick ? chain.KillsThisTick + 1 : 1;
+ chain.KillsThisTick = (chain.LastTick == tick) && string.Equals(chain.LastWeapon, weapon, StringComparison.Ordinal) ? chain.KillsThisTick + 1 : 1;
chain.LastTick = tick;
+ chain.LastWeapon = weapon;
bool trade = false;
if (recentKillsBy.TryGetValue(victim, out List? victimsKills))
@@ -115,6 +120,8 @@ private sealed class KillChain
public int LastTick { get; set; } = -1;
public int KillsThisTick { get; set; }
+
+ public string? LastWeapon { get; set; }
}
private sealed record KillRecord(int Victim, int VictimTeam, double Time);
diff --git a/CounterStrikeProgression.Test/SkillShotTests.cs b/CounterStrikeProgression.Test/SkillShotTests.cs
index 8901c74..0ca341f 100644
--- a/CounterStrikeProgression.Test/SkillShotTests.cs
+++ b/CounterStrikeProgression.Test/SkillShotTests.cs
@@ -149,6 +149,8 @@ public void SituationalKills()
Assert.Contains("triple_kill", Earned(TestHelpers.Kill("ak47") with { ChainedKills = 3 }));
Assert.Contains("rampage", Earned(TestHelpers.Kill("ak47") with { ChainedKills = 6 }));
Assert.Contains("collateral", Earned(TestHelpers.Kill("awp") with { KillsWithThisBullet = 2 }));
+ Assert.Contains("collateral", Earned(TestHelpers.Kill("ak47") with { KillsWithThisBullet = 2 }));
+ Assert.DoesNotContain("collateral", Earned(TestHelpers.Kill("nova") with { KillsWithThisBullet = 2 }), "One shotgun shell is many pellets, not one bullet.");
Assert.Contains("opening_kill", Earned(TestHelpers.Kill("ak47") with { IsOpeningKill = true }));
Assert.Contains("trade", Earned(TestHelpers.Kill("ak47") with { IsTrade = true }));
Assert.Contains("revenge", Earned(TestHelpers.Kill("ak47") with { IsRevenge = true }));
diff --git a/CounterStrikeProgression.Test/TrackerTests.cs b/CounterStrikeProgression.Test/TrackerTests.cs
index 0affd17..ae377cf 100644
--- a/CounterStrikeProgression.Test/TrackerTests.cs
+++ b/CounterStrikeProgression.Test/TrackerTests.cs
@@ -49,6 +49,17 @@ public void KillsOnTheSameTickAreACollateral()
Assert.AreEqual(1, round.RegisterKill(1, TeamA, 12, TeamB, 1.1, 70).KillsWithThisBullet);
}
+ [TestMethod]
+ public void KillsOnTheSameTickWithDifferentWeaponsAreNotOneBullet()
+ {
+ RoundTracker round = new(new ProgressionSettings());
+ round.StartRound();
+
+ Assert.AreEqual(1, round.RegisterKill(1, TeamA, 10, TeamB, 1.0, 64, "molotov").KillsWithThisBullet);
+ Assert.AreEqual(1, round.RegisterKill(1, TeamA, 11, TeamB, 1.0, 64, "ak47").KillsWithThisBullet, "A molotov kill and a rifle kill on one tick counted as one bullet.");
+ Assert.AreEqual(2, round.RegisterKill(1, TeamA, 12, TeamB, 1.0, 64, "ak47").KillsWithThisBullet, "Two rifle kills on one tick are one bullet.");
+ }
+
[TestMethod]
public void KillingATeammatesKillerQuicklyIsATrade()
{
diff --git a/CounterStrikeProgression/CombatHandlers.cs b/CounterStrikeProgression/CombatHandlers.cs
index df1ae6c..3ed4abc 100644
--- a/CounterStrikeProgression/CombatHandlers.cs
+++ b/CounterStrikeProgression/CombatHandlers.cs
@@ -256,13 +256,16 @@ private HookResult OnPlayerDeath(EventPlayerDeath @event, GameEventInfo info)
private void CreditKill(EventPlayerDeath @event, CCSPlayerController attacker, CCSPlayerController victim, bool teammatesAreEnemies)
{
+ CCSPlayerPawn? attackerPawn = GameState.PawnOf(attacker);
+ WeaponInfo? weapon = GameState.ResolveWeapon(@event.Weapon, attackerPawn);
KillContext context = services.Round.RegisterKill(
attacker.Slot,
GameState.SideOf(attacker, teammatesAreEnemies),
victim.Slot,
GameState.SideOf(victim, teammatesAreEnemies),
Server.CurrentTime,
- Server.TickCount);
+ Server.TickCount,
+ weapon?.Key.WeakString ?? @event.Weapon);
CreditAssist(@event, victim);
if (!ProgressionServices.IsTeammate(attacker, victim) && (Sessions.Get(attacker) is { Progress: not null } lifeSession))
@@ -270,8 +273,6 @@ private void CreditKill(EventPlayerDeath @event, CCSPlayerController attacker, C
lifeSession.Life.Kill(victim.IsBot);
}
- CCSPlayerPawn? attackerPawn = GameState.PawnOf(attacker);
- WeaponInfo? weapon = GameState.ResolveWeapon(@event.Weapon, attackerPawn);
if ((weapon is not null) && !ProgressionServices.IsTeammate(attacker, victim) && ((services.KillMaps is not null) || (services.Site is not null))
&& (KillMapRecorder.PointOf(Server.MapName ?? string.Empty, attacker, Sessions.Get(attacker)?.SteamId ?? 0, victim, Sessions.Get(victim)?.SteamId ?? 0, weapon, @event.Headshot) is { } point))
{