Teamfight Detection
gem detects teamfights by looking for windows of concentrated hero-vs-hero combat in the combat log. match.teamfights is a list of Teamfight objects, one per detected window.
What a teamfight is
gem detects a fight by:
- Scanning for hero death events in the combat log.
- Opening a window of ±15 seconds around each death.
- Merging windows that share deaths — extending the fight while combat continues.
- Optionally splitting simultaneous skirmishes in different parts of the map using spatial clustering (when position data is available).
The result is a list of non-overlapping fight windows with per-participant statistics.
Accessing teamfight data
import gem
match = gem.parse("my_replay.dem")
for fight in match.teamfights:
duration = (fight.end_tick - fight.start_tick) / 30
print(
f"Fight at tick {fight.start_tick:,}–{fight.end_tick:,} "
f"({duration:.0f}s) "
f"{fight.deaths} deaths "
f"winner: {fight.winner} "
f"({fight.radiant_kills}–{fight.dire_kills})"
)Teamfight fields
fight.start_tick # int: tick the window opens
fight.end_tick # int: tick the window closes
fight.last_death_tick # int: tick of the final death
fight.deaths # int: total hero deaths in the window
fight.radiant_kills # int: hero kills scored by Radiant
fight.dire_kills # int: hero kills scored by Dire
fight.winner # str: "radiant", "dire", "draw", or "unknown"
fight.centroid_x # float | None: weighted mean X of all deaths
fight.centroid_y # float | None: weighted mean Y of all deaths
fight.players # list[TeamfightPlayer]: one per slot (0–9)Per-participant stats
for player in fight.players:
print(player.player_id) # int: player slot 0–9
print(player.deaths) # int: deaths in this fight window
print(player.damage_dealt) # int: damage dealt to enemy heroes
print(player.damage_taken) # int: damage taken from enemy heroes
print(player.healing) # int: healing dealt to allied heroes (not self)
print(player.buybacks) # int: buybacks used in this window
print(player.gold_delta) # int: net gold change during the fight
print(player.xp_delta) # int: net XP change during the fight
print(player.ability_uses) # dict[str, int]: ability name → use count
print(player.item_uses) # dict[str, int]: item name → use countWhat counts as a participant
A hero is only an active participant in a fight if they had direct hero-vs-hero combat in the window:
deaths > 0— died in the window, ORdamage_dealt > 0— dealt damage to an enemy hero, ORdamage_taken > 0— took damage from an enemy hero, ORhealing > 0— healed a different allied hero
Not counted as participation:
- Dealing damage to creeps, towers, or Roshan inside the window.
- Self-healing (tango, salve, faerie fire) — these show
attacker == targetand are excluded. - Neutrals or creeps attacking a hero — damage must come from
attacker_is_hero=True.
Applying your own threshold
gem returns all detected windows without a minimum death threshold. Apply your own filter to match OpenDota's convention (3+ deaths = teamfight):
significant_fights = [f for f in match.teamfights if f.deaths >= 3]Finding the fight at a specific tick
Use gem.teamfight_at_tick() to look up which fight contains a given tick — useful when you have a combat log event and want its fight context:
fight = gem.teamfight_at_tick(match, entry.tick)
if fight:
print(f"Event happened during a fight: {fight.winner} won ({fight.deaths} deaths)")Finding heroes near a fight location
Use gem.heroes_near() to find all heroes within a radius of the fight centroid at initiation time — useful for answering "who was in position to join?":
if fight.centroid_x is not None:
nearby = gem.heroes_near(match, fight.start_tick,
fight.centroid_x, fight.centroid_y,
radius=2000)
for player in nearby:
print(f"{player.hero_name} was near fight start")HTML teamfight report
The Teamfights tab in examples/match_report.py generates a self-contained report with:
- A minimap showing fight locations
- Hero icon timelines
- Per-fight combat log with AoE spells collapsed into grouped cast rows
- Live slider filters for minimum deaths and minimum participant count
python examples/match_report.py my_replay.demgem implementation
Source: src/gem/extractors/teamfights.py
detect_teamfights(combat_log, hero_to_slot, player_snapshots, slot_to_team) is the main function. It runs four passes:
- Pass 1 — detect fight windows from hero death events.
- Pass 2 — accumulate per-player stats (damage, healing, deaths, kills) and populate
radiant_kills/dire_kills. - Pass 3 — compute XP deltas from bracketing snapshots.
- Pass 4 — set
winnerfrom kill counts.