Combat Log Guide¶
AI Summary
Parse structured combat log entries with parse_combat_log(). Use CombatLogType enum for filtering (DAMAGE, HEAL, MODIFIER_ADD, DEATH, ABILITY, ITEM, PURCHASE, etc.). 80+ fields per entry including health tracking, stun/slow durations, assist players, damage types, hero levels, and location. Use heroes_only=True for hero-related entries. Additional enums: DamageType (PHYSICAL, MAGICAL, PURE), Team (RADIANT, DIRE). The game_time field provides accurate in-game clock time (can be negative for pre-game events). The value_name field resolves item names for PURCHASE events.
Overview¶
The combat log provides structured data about damage, healing, deaths, and other combat-related events with rich metadata.
from python_manta import Parser
parser = Parser("match.dem")
result = parser.parse(combat_log={"max_entries": 100})
for entry in result.combat_log.entries:
print(f"[{entry.game_time_str}] {entry.type_name}: {entry.attacker_name} -> {entry.target_name}")
Combat Log Types¶
| ID | Type Name | Description |
|---|---|---|
| 0 | DOTA_COMBATLOG_DAMAGE | Damage dealt to units |
| 1 | DOTA_COMBATLOG_HEAL | Healing received |
| 2 | DOTA_COMBATLOG_MODIFIER_ADD | Buff/debuff applied |
| 3 | DOTA_COMBATLOG_MODIFIER_REMOVE | Buff/debuff removed |
| 4 | DOTA_COMBATLOG_DEATH | Unit death |
| 5 | DOTA_COMBATLOG_ABILITY | Ability cast |
| 6 | DOTA_COMBATLOG_ITEM | Item used |
| 7 | DOTA_COMBATLOG_LOCATION | Location event |
| 8 | DOTA_COMBATLOG_GOLD | Gold gained |
| 9 | DOTA_COMBATLOG_GAME_STATE | Game state change |
| 10 | DOTA_COMBATLOG_XP | Experience gained |
| 11 | DOTA_COMBATLOG_PURCHASE | Item purchased |
| 12 | DOTA_COMBATLOG_BUYBACK | Buyback used |
| 13 | DOTA_COMBATLOG_ABILITY_TRIGGER | Ability triggered |
| 14 | DOTA_COMBATLOG_PLAYERSTATS | Player statistics |
| 15 | DOTA_COMBATLOG_MULTIKILL | Multi-kill event |
| 16 | DOTA_COMBATLOG_KILLSTREAK | Kill streak |
| 17 | DOTA_COMBATLOG_TEAM_BUILDING_KILL | Building destroyed |
| 18 | DOTA_COMBATLOG_FIRST_BLOOD | First blood |
| 19 | DOTA_COMBATLOG_MODIFIER_REFRESH | Modifier refreshed |
| 20 | DOTA_COMBATLOG_NEUTRAL_CAMP_STACK | Camp stacked |
| 21 | DOTA_COMBATLOG_PICKUP_RUNE | Rune picked up |
| 22 | DOTA_COMBATLOG_REVEALED_INVISIBLE | Invisibility revealed |
| 23 | DOTA_COMBATLOG_HERO_SAVED | Hero saved from death |
| 24 | DOTA_COMBATLOG_MANA_RESTORED | Mana restored |
| 25 | DOTA_COMBATLOG_HERO_LEVELUP | Hero level up |
| 26 | DOTA_COMBATLOG_BOTTLE_HEAL_ALLY | Bottle heal ally |
| 27 | DOTA_COMBATLOG_ENDGAME_STATS | End game statistics |
| 28 | DOTA_COMBATLOG_INTERRUPT_CHANNEL | Channel interrupted |
| 29 | DOTA_COMBATLOG_ALLIED_GOLD | Allied gold |
| 30 | DOTA_COMBATLOG_AEGIS_TAKEN | Aegis taken |
| 31 | DOTA_COMBATLOG_MANA_DAMAGE | Mana burned |
| 32 | DOTA_COMBATLOG_PHYSICAL_DAMAGE_PREVENTED | Physical damage blocked |
| 33 | DOTA_COMBATLOG_UNIT_SUMMONED | Unit summoned |
| 34 | DOTA_COMBATLOG_ATTACK_EVADE | Attack evaded |
| 35 | DOTA_COMBATLOG_TREE_CUT | Tree cut |
| 36 | DOTA_COMBATLOG_SUCCESSFUL_SCAN | Successful scan |
| 37 | DOTA_COMBATLOG_END_KILLSTREAK | Kill streak ended |
| 38 | DOTA_COMBATLOG_BLOODSTONE_CHARGE | Bloodstone charge |
| 39 | DOTA_COMBATLOG_CRITICAL_DAMAGE | Critical damage |
| 40 | DOTA_COMBATLOG_SPELL_ABSORB | Spell absorbed |
| 41 | DOTA_COMBATLOG_UNIT_TELEPORTED | Unit teleported |
| 42 | DOTA_COMBATLOG_KILL_EATER_EVENT | Kill eater (gem) event |
| 43 | DOTA_COMBATLOG_NEUTRAL_ITEM_EARNED | Neutral item earned |
| 44 | DOTA_COMBATLOG_TELEPORT_INTERRUPTED | Teleport interrupted |
Filtering by Type¶
Damage Only¶
result = parser.parse_combat_log("match.dem", types=[0], max_entries=500)
for entry in result.entries:
print(f"[{entry.game_time_str}] {entry.attacker_name} dealt {entry.value} damage to {entry.target_name}")
if entry.inflictor_name:
print(f" via {entry.inflictor_name}")
Healing Only¶
result = parser.parse_combat_log("match.dem", types=[1], max_entries=200)
for entry in result.entries:
print(f"[{entry.game_time_str}] {entry.target_name} healed for {entry.value}")
if entry.inflictor_name:
print(f" from {entry.inflictor_name}")
Deaths Only¶
result = parser.parse_combat_log("match.dem", types=[4], max_entries=100)
for entry in result.entries:
print(f"[{entry.game_time_str}] {entry.target_name} was killed by {entry.attacker_name}")
Multiple Types¶
# Damage and deaths together
result = parser.parse_combat_log("match.dem", types=[0, 4], max_entries=500)
for entry in result.entries:
if entry.type == 0:
print(f"[{entry.game_time_str}] DAMAGE: {entry.attacker_name} -> {entry.target_name} ({entry.value})")
elif entry.type == 4:
print(f"[{entry.game_time_str}] DEATH: {entry.target_name} killed by {entry.attacker_name}")
Hero-Only Filtering¶
Filter to only include entries where the attacker or target is a hero:
result = parser.parse_combat_log("match.dem", heroes_only=True, max_entries=500)
for entry in result.entries:
hero_indicator = ""
if entry.is_attacker_hero:
hero_indicator += "[HERO ATK] "
if entry.is_target_hero:
hero_indicator += "[HERO TGT] "
print(f"{hero_indicator}{entry.attacker_name} -> {entry.target_name}")
Combining Filters¶
# Hero damage only
result = parser.parse_combat_log(
"match.dem",
types=[0], # Damage
heroes_only=True, # Hero involvement
max_entries=500
)
Entry Fields (80+ Total)¶
Each CombatLogEntry contains comprehensive data for fight analysis:
Core Fields¶
| Field | Type | Description |
|---|---|---|
tick |
int | Game tick (~30/second) |
net_tick |
int | Network tick |
type |
int | Combat log type ID (0-44) |
type_name |
str | Human-readable type name |
game_time |
float | Game time in seconds (0:00 = horn, negative for pre-game) |
game_time_str |
str | Formatted game time (e.g., "-0:40", "5:32") |
Participant Fields¶
| Field | Type | Description |
|---|---|---|
target_name |
str | Target unit name (e.g., "npc_dota_hero_pudge") |
target_source_name |
str | Target's source name |
attacker_name |
str | Attacker unit name |
damage_source_name |
str | Damage source name |
inflictor_name |
str | Ability/item that caused this |
Participant Flags¶
| Field | Type | Description |
|---|---|---|
is_attacker_illusion |
bool | Attacker is an illusion |
is_attacker_hero |
bool | Attacker is a hero |
is_target_illusion |
bool | Target is an illusion |
is_target_hero |
bool | Target is a hero |
is_target_building |
bool | Target is a building |
Combat Values¶
| Field | Type | Description |
|---|---|---|
value |
int | Damage/heal amount (or string table index for PURCHASE events) |
value_name |
str | Resolved name from CombatLogNames (e.g., item name for PURCHASE) |
health |
int | Target HP after this event |
damage_type |
int | Damage type (physical/magical/pure) |
damage_category |
int | Damage category |
CC Durations¶
| Field | Type | Description |
|---|---|---|
stun_duration |
float | Stun duration in seconds |
slow_duration |
float | Slow duration in seconds |
modifier_duration |
float | Total modifier duration |
modifier_elapsed_duration |
float | How long modifier has been active |
Location¶
| Field | Type | Description |
|---|---|---|
location_x |
float | X coordinate on map |
location_y |
float | Y coordinate on map |
Assist Tracking¶
| Field | Type | Description |
|---|---|---|
assist_player0 |
int | First assist player ID |
assist_player1 |
int | Second assist player ID |
assist_player2 |
int | Third assist player ID |
assist_player3 |
int | Fourth assist player ID |
assist_players |
List[int] | All assist player IDs |
Modifier Fields¶
| Field | Type | Description |
|---|---|---|
root_modifier |
bool | Is a root effect |
silence_modifier |
bool | Is a silence effect |
aura_modifier |
bool | Is an aura |
armor_debuff_modifier |
bool | Is armor reduction |
motion_controller_modifier |
bool | Is motion control (knockback) |
invisibility_modifier |
bool | Grants invisibility |
hidden_modifier |
bool | Is hidden modifier |
modifier_hidden |
bool | Modifier is hidden from UI |
modifier_purged |
bool | Modifier was purged |
no_physical_damage_modifier |
bool | Blocks physical damage |
modifier_ability |
int | Ability index in CombatLogNames |
modifier_ability_name |
str | Resolved ability name that applied this modifier |
modifier_purge_ability |
int | Ability index that purged this modifier |
modifier_purge_ability_name |
str | Resolved ability name that purged this modifier |
modifier_purge_npc |
int | NPC index that purged this modifier |
modifier_purge_npc_name |
str | Resolved NPC name that purged this modifier |
Ability Info¶
| Field | Type | Description |
|---|---|---|
ability_level |
int | Ability level (1-4+) |
is_ability_toggle_on |
bool | Ability toggled on |
is_ability_toggle_off |
bool | Ability toggled off |
is_ultimate_ability |
bool | Is an ultimate ability |
inflictor_is_stolen_ability |
bool | Ability was stolen (Rubick) |
spell_generated_attack |
bool | Attack from spell |
uses_charges |
bool | Ability uses charges |
Kill/Death Info¶
| Field | Type | Description |
|---|---|---|
spell_evaded |
bool | Spell was evaded |
long_range_kill |
bool | Long range kill |
will_reincarnate |
bool | Target will reincarnate (Aegis/WK) |
total_unit_death_count |
int | Total deaths of this unit type |
heal_from_lifesteal |
bool | Heal is from lifesteal |
is_heal_save |
bool | Heal prevented death |
Hero State¶
| Field | Type | Description |
|---|---|---|
attacker_hero_level |
int | Attacker's hero level (from entity state) |
target_hero_level |
int | Target's hero level (from entity state) |
attacker_has_scepter |
bool | Attacker has Aghanim's Scepter |
attacker_team |
int | Attacker team (2=Radiant, 3=Dire) |
target_team |
int | Target team |
Hero Levels Now Available
As of v1.4.5.4, attacker_hero_level and target_hero_level are populated from entity state during parsing. Previously these were always 0 (Dota 2's protobuf doesn't populate them), but we now inject levels from m_iCurrentLevel entity property.
- 100% of hero deaths have
target_hero_levelpopulated - 94%+ have
attacker_hero_level(non-hero attackers like summons/neutrals are 0)
Visibility¶
| Field | Type | Description |
|---|---|---|
is_visible_radiant |
bool | Visible to Radiant team |
is_visible_dire |
bool | Visible to Dire team |
at_night_time |
bool | Event occurred at night |
Economy¶
| Field | Type | Description |
|---|---|---|
xp |
int | XP gained/reason |
gold |
int | Gold gained/reason |
last_hits |
int | Last hits at time |
networth |
int | Player networth |
xpm |
int | XP per minute |
gpm |
int | Gold per minute |
Additional¶
| Field | Type | Description |
|---|---|---|
stack_count |
int | Modifier stack count |
building_type |
int | Building type ID |
neutral_camp_type |
int | Neutral camp type |
neutral_camp_team |
int | Neutral camp team |
rune_type |
int | Rune type |
obs_wards_placed |
int | Observer wards placed |
regenerated_health |
float | Health regenerated |
target_is_self |
bool | Target is self |
Common Use Cases¶
DPS Analysis¶
from collections import defaultdict
result = parser.parse_combat_log("match.dem", types=[0], heroes_only=True, max_entries=5000)
damage_dealt = defaultdict(int)
for entry in result.entries:
if entry.is_attacker_hero:
damage_dealt[entry.attacker_name] += entry.value
print("Total Damage Dealt by Hero:")
for hero, damage in sorted(damage_dealt.items(), key=lambda x: -x[1]):
print(f" {hero}: {damage:,}")
Kill Feed Reconstruction¶
parser = Parser("match.dem")
result = parser.parse(combat_log={"types": [4], "heroes_only": True, "max_entries": 100})
print("Kill Feed:")
print("-" * 60)
for entry in result.combat_log.entries:
# Hero levels are now populated from entity state
attacker_lvl = f" (lvl {entry.attacker_hero_level})" if entry.attacker_hero_level > 0 else ""
target_lvl = f" (lvl {entry.target_hero_level})" if entry.target_hero_level > 0 else ""
print(f"[{entry.game_time_str}] {entry.attacker_name}{attacker_lvl} killed {entry.target_name}{target_lvl}")
Output:
Kill Feed:
------------------------------------------------------------
[00:10] npc_dota_hero_pugna (lvl 1) killed npc_dota_hero_troll_warlord (lvl 1)
[03:36] npc_dota_hero_pugna (lvl 2) killed npc_dota_hero_hoodwink (lvl 2)
[05:09] npc_dota_hero_bristleback (lvl 3) killed npc_dota_hero_pugna (lvl 3)
Ability Usage Tracking¶
from collections import defaultdict
result = parser.parse_combat_log("match.dem", types=[5], max_entries=1000)
ability_usage = defaultdict(int)
for entry in result.entries:
if entry.inflictor_name:
ability_usage[entry.inflictor_name] += 1
print("Most Used Abilities:")
for ability, count in sorted(ability_usage.items(), key=lambda x: -x[1])[:20]:
print(f" {ability}: {count}")
Gold Economy¶
from collections import defaultdict
result = parser.parse_combat_log("match.dem", types=[7], max_entries=2000)
gold_gained = defaultdict(int)
for entry in result.entries:
if entry.is_target_hero:
gold_gained[entry.target_name] += entry.gold
print("Gold Gained:")
for hero, gold in sorted(gold_gained.items(), key=lambda x: -x[1]):
print(f" {hero}: {gold:,}")
Healing Analysis¶
from collections import defaultdict
result = parser.parse_combat_log("match.dem", types=[1], max_entries=2000)
healing_received = defaultdict(int)
healing_sources = defaultdict(lambda: defaultdict(int))
for entry in result.entries:
healing_received[entry.target_name] += entry.value
if entry.inflictor_name:
healing_sources[entry.target_name][entry.inflictor_name] += entry.value
print("Healing Received:")
for unit, total in sorted(healing_received.items(), key=lambda x: -x[1])[:10]:
print(f"\n{unit}: {total:,} total")
for source, amount in sorted(healing_sources[unit].items(), key=lambda x: -x[1])[:3]:
print(f" {source}: {amount:,}")
Important Notes¶
Game Time¶
The game_time field provides the in-game clock time (what you see on screen). The horn sounds at game_time=0, and pre-game events have negative times.
| Field | Description |
|---|---|
game_time |
In-game clock time in seconds (0:00 = horn, negative for pre-game) |
game_time_str |
Formatted time string (e.g., "-0:40", "5:32") |
from python_manta import Parser
parser = Parser("match.dem")
result = parser.parse(combat_log={"types": [11], "max_entries": 100}) # PURCHASE events
for entry in result.combat_log.entries:
# Use game_time_str for formatted output
print(f"[{entry.game_time_str}] {entry.value_name}")
Pre-Game Events (Negative Game Time)¶
Events during the 90-second pre-game countdown have negative game_time values. Use is_pre_horn property to check:
parser = Parser("match.dem")
result = parser.parse(combat_log={"types": [11], "max_entries": 200}) # PURCHASE
# Pre-game purchases (during countdown)
pregame = [e for e in result.combat_log.entries if e.is_pre_horn]
print(f"Pre-game purchases: {len(pregame)}")
for entry in pregame[:5]:
hero = entry.target_name.replace("npc_dota_hero_", "")
item = entry.value_name.replace("item_", "")
print(f"[{entry.game_time_str}] {hero}: {item}")
# Output:
# [-1:29] antimage: ward_observer
# [-1:28] troll_warlord: tango
# [-1:27] crystal_maiden: blood_grenade
Game Start Tick¶
The CombatLogResult includes game_start_tick which is the tick when the horn sounded (game_time=0):
parser = Parser("match.dem")
result = parser.parse(combat_log={"types": [18], "max_entries": 1}) # First blood
print(f"Game started at tick: {result.combat_log.game_start_tick}")
if result.combat_log.entries:
entry = result.combat_log.entries[0]
print(f"First blood at {entry.game_time_str} game time")
### Illusion Filtering
```python
# Filter out illusion damage for accurate stats
result = parser.parse_combat_log("match.dem", types=[0], heroes_only=True, max_entries=5000)
real_damage = [
entry for entry in result.entries
if not entry.is_attacker_illusion and not entry.is_target_illusion
]
print(f"Total entries: {len(result.entries)}")
print(f"Real damage entries (no illusions): {len(real_damage)}")
Combat Log vs Game Events¶
| Aspect | Combat Log | Game Events |
|---|---|---|
| API | parse_combat_log() |
parse_game_events() |
| Structure | Fixed schema with 80+ fields | Variable fields per event type |
| Types | 45 log types | 364 event types |
| Best for | Detailed damage/heal analysis | Discrete game occurrences |
| Timing | Full match (game_time is in-game clock) | Full match |
| Filtering | By type ID, heroes_only | By event name |
Use combat log for continuous combat data (DPS, healing totals) and game events for discrete occurrences (tower kills, rune pickups).
Available Data Fields¶
The combat log exposes raw data that can be used by other tools for analysis or narrative generation.
Data Available Per Event¶
| Data Point | Field | Notes |
|---|---|---|
| HP after event | health |
Target's HP after this event |
| Damage/heal amount | value |
Raw numeric value |
| Ability/item name | inflictor_name |
Internal name (e.g., "pugna_nether_blast") |
| Item name (PURCHASE) | value_name |
Resolved item name for PURCHASE events |
| Ability level | ability_level |
1-4+ |
| Assist player IDs | assist_players |
List of player IDs |
| Stun duration | stun_duration |
Seconds |
| Slow duration | slow_duration |
Seconds |
| Root applied | root_modifier |
Boolean |
| Lifesteal heal | heal_from_lifesteal |
Boolean |
| Will reincarnate | will_reincarnate |
Boolean (Aegis/WK) |
| Night time | at_night_time |
Boolean |
| Has Aghanim's | attacker_has_scepter |
Boolean |
| Hero levels | attacker_hero_level, target_hero_level |
Integer |
| Position | location_x, location_y |
Map coordinates |
| Game time | game_time |
In-game clock time in seconds (negative for pre-game) |
| Game time (formatted) | game_time_str |
Formatted time string (e.g., "-0:40", "5:32") |
Data NOT Available in Combat Log¶
| Data Point | Notes |
|---|---|
stack_count |
Always 0 - Valve doesn't populate this field |
uses_charges |
Always false - Valve doesn't populate this field |
| Cooldowns | Not in combat log |
| Mana costs | Not in combat log |
| Exact max HP | Only current HP after event |
Getting Item Charges
The combat log stack_count field is NOT populated by Valve. To get actual item charges (e.g., Magic Stick/Wand), use entity queries instead:
from python_manta import MantaParser
parser = MantaParser()
def get_magic_stick_charges(demo_path: str, at_tick: int = 0) -> dict:
"""
Get magic stick/wand charges for all players at a specific game tick.
Args:
demo_path: Path to the .dem file
at_tick: Game tick to query (0 = end of game)
Returns:
Dict mapping player_id -> charges
"""
result = parser.query_entities(
demo_path,
class_names=['CDOTA_Item_MagicStick', 'CDOTA_Item_MagicWand'],
property_filter=['m_iCurrentCharges', 'm_iPlayerOwnerID'],
at_tick=at_tick
)
player_charges = {}
for entity in result.entities:
owner = entity.properties.get('m_iPlayerOwnerID', -1)
charges = entity.properties.get('m_iCurrentCharges', 0)
if owner >= 0:
player_charges[owner] = max(player_charges.get(owner, 0), charges)
return player_charges
# Get charges at a specific tick
charges = get_magic_stick_charges("match.dem", at_tick=50000)
# {0: 6, 2: 18, 4: 7, 6: 6, 8: 1, 10: 15, 12: 4, 14: 20, 16: 0, 18: 5}
Alternative: Calculate charges from HEAL event value (Magic Stick/Wand heal 15 HP per charge):
Deriving Respawn Events¶
The combat log doesn't have explicit respawn events, but you can derive them from DEATH events using the derive_respawn_events() utility.
Basic Usage¶
from python_manta import Parser, derive_respawn_events, CombatLogType
parser = Parser("match.dem")
# Get all hero deaths
result = parser.parse(combat_log={
"types": [CombatLogType.DEATH],
"heroes_only": True,
"max_entries": 1000
})
# Derive respawn events
respawns = derive_respawn_events(result.combat_log)
for r in respawns[:10]:
print(f"{r.hero_display_name} died at {r.death_game_time_str}, "
f"respawns after {r.respawn_duration:.0f}s")
HeroRespawnEvent Fields¶
Each HeroRespawnEvent includes:
| Field | Type | Description |
|---|---|---|
hero_name |
str | Internal hero name (e.g., "npc_dota_hero_axe") |
hero_display_name |
str | Display name (e.g., "Axe") |
death_tick |
int | Tick when hero died |
death_game_time |
float | Game time when hero died |
death_game_time_str |
str | Formatted death time |
respawn_tick |
int | Estimated respawn tick |
respawn_game_time |
float | Estimated respawn game time |
respawn_game_time_str |
str | Formatted respawn time |
respawn_duration |
float | Respawn duration in seconds |
killer_name |
str | Who killed the hero |
hero_level |
int | Hero level at death (if available) |
team |
int | Hero's team (2=Radiant, 3=Dire) |
will_reincarnate |
bool | True if Aegis/Wraith King ult |
location_x |
float | Death X coordinate |
location_y |
float | Death Y coordinate |
Hero Levels for Respawn Calculation¶
As of v1.4.5.4, target_hero_level is now populated directly in combat log entries, so respawn times are calculated accurately without needing to provide levels manually:
# Hero levels are now available directly in combat log
respawns = derive_respawn_events(result.combat_log)
for r in respawns[:5]:
print(f"{r.hero_display_name} (lvl {r.hero_level}) died at {r.death_game_time_str}, "
f"respawns after {r.respawn_duration:.0f}s")
You can still provide custom hero levels if needed (e.g., for edge cases or to override):
# Optional: provide custom levels
hero_levels = {"npc_dota_hero_axe": 25}
respawns = derive_respawn_events(result.combat_log, hero_levels=hero_levels)
Respawn Time Formula¶
Respawn time is calculated as: min(4 + (level × 2), 100) seconds.
| Level | Respawn Time |
|---|---|
| 1-5 | 6-14 seconds |
| 10 | 24 seconds |
| 15 | 34 seconds |
| 20 | 44 seconds |
| 25 | 54 seconds |
| 30 | 64 seconds |
Aegis and Wraith King
When will_reincarnate is True (Aegis or Wraith King ult), the hero respawns at the death location after ~5 seconds instead of at fountain.