Calculate hero stats¶
Use demo.calculate_hero_stats() for values at selected demo ticks.
Boon uses replay state, a boon-data catalog, and a calculation rule.
Select stats with these strings or enum members. Names are case-sensitive.
String |
Python enum |
Rust enum |
Unit |
|---|---|---|---|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
HeroStat.AMMO is an alias for HeroStat.CLIP_SIZE. The string "ammo" is not accepted.
Without stats, the query selects capacity. Use stats=list(HeroStat) to select all supported stats.
Percentage values use percentage points: 20 means 20%.
Capacity stays finite during unlimited ammo. Use ammo snapshots for remaining rounds.
Select the game data¶
boon versions
boon get GAME_VERSION
Set data_version to the replay’s Deadlock client version.
Boon does integrity checks of installed files or downloads that exact version.
Boon does not select the version from the demo header.
Catalogs must include record_key, definition_path, and stat_changes.
After a catalog release changes, use boon get VERSION --force to replace local files.
Python¶
from boon import Demo, HeroStat, StatMode
demo = Demo("108575009.dem", preload=False)
result = demo.calculate_hero_stats(
ticks=[187554, 187600],
steam_ids=[76561198037652386], # McGinnis in this demo.
data_version="6712",
stats=[HeroStat.CLIP_SIZE, HeroStat.FIRE_RATE],
mode=StatMode.CURRENT,
explain=True,
strict=False,
)
print(result.values)
print(result.contributions)
print(result.metadata)
Without rulesets, each stat uses its supported v1 rule.
An explicit mapping must contain one supported rule for each requested stat:
from boon import rulesets
selected_rules = {
HeroStat.CLIP_SIZE: rulesets.clip_size.v1,
HeroStat.FIRE_RATE: rulesets.fire_rate.v1,
}
# Use rulesets=selected_rules with these two stats.
Each rule has a name, version, and documentation date. The date is not a game patch date.
Select baseline or current effects¶
The two hero and ability queries accept these modes:
String |
Python enum |
Rust enum |
Inputs |
|---|---|---|---|
|
|
|
Supported active effects, including temporary buffs and debuffs. Default. |
|
|
|
Hero values, owned passive effects, and permanent changes. |
The two modes use the selected tick and keep permanent penalties. They also apply to dependent inputs, such as spirit power for ammo. Items sold before that tick do not contribute. Unknown effect roles give partial baseline values with diagnostics. An untimed modifier does not prove a passive effect.
Movement values do not simulate firing, crouching, bullet-hit slows, or sprint acceleration.
gravity_scale has only a recorded current value.
A baseline gravity query raises an error, or returns null with strict=False.
Select ticks and players¶
ticks accepts an integer or a sequence of integers from 0 through 2147483646.
Boon removes duplicate ticks and reads state after each tick.
Missing ticks cause an error. Multiple ticks share a parser pass.
Use the tick passed to demo_gototick; the viewer’s pause message can show a different server tick.
Use steam_ids for Steam accounts or heroes for hero IDs at each tick.
Get Steam IDs from demo.players. The two filters apply when set.
Without filters, the query selects all players with a hero. An empty filter selects no players.
A requested Steam ID without a selected hero causes an error at that tick.
Results¶
|
Contents |
|---|---|
|
|
|
Demo tick (Int32) |
|
Steam ID (UInt64), or null |
|
Hero ID at this tick (Int64) |
|
Stat string |
|
Result (Float64), or null |
|
Unit from the stat table |
|
Equation ID, such as |
|
|
|
Missing inputs or assumed links; null when none apply |
explain=True adds source rows to result.contributions.
Rows identify inputs, catalog paths, values, and modifier serials.
Intermediate inputs, such as spirit_power, are not extra stat bonuses.
Purchase-cost rows use souls, not percentage points.
metadata records the mode, selected client version, source commit, catalog snapshot, and rules.
Join player results with steam_id. Include tick for sampled rows and stat for stat rows.
Do not join null Steam IDs as one player.
Weapon damage¶
weapon_damage = sum(weapon damage percentage bonuses)
The result is a global bonus percentage, before rounding. Inputs include shop bonuses, boon growth, recorded totals, and mapped effects. Counter bindings can add earned rewards, such as Bloodscent’s kills and assists. Markers on another player do not transfer the owner’s reward.
The result does not include flat bullet damage, critical hits, falloff, and target-specific bonuses. See weapon limits.
Ammo equation¶
ceil((base ammo + flat bonuses) * (1 + sum(percent bonuses) / 100))
For base 20, flat bonus 10, and percentage bonuses 15 and 4, capacity is 36 rounds.
Weapon values come from the hero’s primary weapon record, including m_mapWeaponInfos.primary.
Hero spirit scaling comes from m_mapScalingStats.EClipSize.
Boon does not substitute an alternate weapon for a missing primary definition.
Permanent bonuses and penalties use recorded stat types. Corrupted ability bonuses are unsupported and are not detected. Powerup values use catalog ranges and match time at application. See ammo limits.
Bullet velocity equation¶
base speed in Source units/s * (1 + sum(percent bonuses) / 100) * 0.0254
Boon reads m_flBulletSpeed and MODIFIER_VALUE_BONUS_BULLET_SPEED_PERCENT.
Results use m/s. The conversion factor changes inches to meters.
For base 8000 and bonuses 60% and 25%, the result is 375.92 m/s.
The result describes the primary gun, without flight curves, beam behavior, or alternate fire. A nonzero base-speed override stays unresolved because its priority rule is unknown.
Falloff range¶
multiplier = 1 + percent_bonus / 100
start_meters = base_start_source_units * 0.0254 * multiplier
end_meters = base_end_source_units * 0.0254 * multiplier
Bases use m_flDamageFalloffStartRange and m_flDamageFalloffEndRange.
Bonuses use MODIFIER_VALUE_BONUS_ATTACK_RANGE_PERCENT.
V1 supports at most one nonzero bonus. Multiple bonuses stay unresolved.
Missing, negative, or reversed base ranges also stay unresolved.
Start is where damage begins to decrease. End is where the falloff penalty reaches its maximum. End is not maximum bullet travel. Ability range does not change these endpoints. Compare the equal scaling model with viewer values, particularly for beams.
Melee damage¶
boon_growth = standard_boon_count * light_melee_gain_per_boon
base_with_growth = base_damage + boon_growth * (base_damage / base_light_damage)
spirit_bonus = spirit_power * hero_scaling_coefficient
final_damage = (base_with_growth + spirit_bonus)
* (1 + (weapon_bonus_percent / 2 + melee_bonus_percent) / 100)
Bases use ELightMeleeDamage and EHeavyMeleeDamage.
Growth uses m_mapLevelInfo and MODIFIER_VALUE_BASE_MELEE_DAMAGE_FROM_LEVEL.
Spirit coefficients come from the matching m_mapScalingStats entries.
Heavy melee uses the catalog’s heavy/light base ratio.
For light melee, that ratio is 1.
The weapon-damage inputs also apply at half strength.
Melee bonuses use MODIFIER_VALUE_MELEE_DAMAGE_INCREASE.
Results do not include target resistance, immunity, and effects that occur on a hit.
Nonzero melee or all-damage multipliers stay unresolved.
The equation follows player observations of weapon scaling
and boon growth.
The engine equation and scaling order still have unverified assumptions.
Rust uses rulesets::melee_damage::LIGHT_V1 and HEAVY_V1.
Melee distance bonus¶
sum(heavy-melee travel percentage bonuses)
Inputs use MODIFIER_VALUE_MELEE_TRAVEL_DISTANCE_PERCENTAGE.
The result is a percentage bonus, not meters or hitbox reach.
Travel also depends on the attack, movement curves, and collisions.
Reload time¶
base reload seconds * (1 + percent adjustment / 100)
Bases use m_reloadDuration; adjustments use MODIFIER_VALUE_RELOAD_SPEED.
A negative adjustment reduces time. For base 2 seconds and -10%, the result is 1.8 seconds.
V1 supports at most one nonzero adjustment.
Multiple adjustments, dynamic overrides, and hero reload scaling stay unresolved.
With m_bReloadSingleBullets=true, the result is seconds for each round.
It does not include the initial delay. Other weapons return a magazine’s reload duration.
This value is not remaining reload time or instant ammo restoration.
Fire-rate increase or decrease¶
remaining = product(1 - each_slow_percent / 100)
modifier_percent = max(-50, sum(bonus_percent) - 100 * (1 - remaining))
Positive MODIFIER_VALUE_FIRE_RATE values add.
Negative values and MODIFIER_VALUE_FIRE_RATE_SLOW supply individual slow factors.
No effects gives 0. The result is the modifier percentage, not shots for each second.
For +18%, +20%, -20%, and -30%, the result is -6%.
A positive modifier multiplies base rate by 1 + modifier_percent / 100.
A negative modifier divides base rate by 1 + abs(modifier_percent) / 100.
Rust provides rulesets::fire_rate::rate_multiplier for this conversion.
Hero spirit scaling uses m_mapScalingStats.EFireRate.
Boon prefers explicit bindings. A unique timed modifier can activate an owner’s unbound conditional property. This assumed link gives partial values with a diagnostic, including Full Auto’s bonus and upgrades. Battle Vest’s intrinsic modifier does not prove its health condition. See Battle Vest.
Slide distance, bullet evasion, and gravity scale¶
Slide distance¶
slide_distance = 100 * (product(1 + each_bonus_percent / 100) - 1)
Inputs use MODIFIER_VALUE_MOVEMENT_SLIDE_DISTANCE_SCALE.
For +35% and +50%, the result is +102.5%.
The result does not describe travel in meters, friction, or slide-turn effects.
Bullet evasion¶
Inputs use MODIFIER_VALUE_BULLET_EVASION.
V1 supports one nonzero chance from 0% through 100%.
Multiple chances stay unresolved.
The exact EvasionPercent property can use an owner’s unique timed effect modifier.
That activation link is an assumption and gives partial values.
Other unbound evasion properties stay unsupported.
Gravity scale¶
The result is the pawn’s recorded m_flGravityScale multiplier.
Boon does not add modifier adjustments or assume 1 for a missing field.
Only current mode has this value; it can lack effective gravity changes.
Stamina and ordinary dashes¶
Stamina¶
capacity = base EStamina + sum(MODIFIER_VALUE_STAMINA)
flat recovery: seconds = 1 / (base_rate + flat_rate)
percent recovery: seconds = 1 / (base_rate * (1 + percent / 100))
stamina is capacity, not remaining resource. Free dashes do not make capacity infinite.
stamina_cooldown is seconds to recover one point, not remaining refill time.
Its base is EStaminaRegenPerSecond.
Rate effects use MODIFIER_VALUE_STAMINA_REGEN_PER_SECOND_ADDITIVE or MODIFIER_VALUE_STAMINA_REGEN_PER_SECOND_PERCENTAGE.
V1 supports flat recovery changes or one nonzero percentage change. Mixed adjustments, multiple percentages, paused recovery, and nonpositive rates stay unresolved. A +25% recovery bonus changes a five-second refill to four seconds.
Dash speed and duration¶
Movement |
Base distance |
Base duration |
|---|---|---|
Ground |
|
|
Air |
|
|
speed = base_distance * (1 + distance_percent / 100) / duration
Speed is a nominal average. Duration stays at the catalog value.
Ground distance uses MODIFIER_VALUE_MOVEMENT_GROUND_DASH_INCREASE_PERCENT and MODIFIER_VALUE_MOVEMENT_GROUND_DASH_REDUCTION_PERCENT.
Air distance uses MODIFIER_VALUE_AIR_MOVE_DISTANCE_INCREASE_PERCENT.
Reductions have negative values.
V1 supports one nonzero distance adjustment for each movement type. Multiple adjustments make speed unresolved but do not prevent a duration result. Flight, movement curves, interrupts, and ability-specific dashes are outside this model. These stats use only explicit effect bindings.
Move speed and sprint speed¶
Equations¶
move_speed = (base_move_speed + sum(flat_move_adjustments)) * (1 + move_percent / 100)
sprint_speed = base_sprint_speed + sum(sprint_bonuses)
full_sprint_speed = move_speed + sprint_speed
Flat movement bonuses and penalties add. Sprint bonuses also add. For base movement 6.4 m/s and bonuses +2 and +3, movement is 11.4 m/s. For base sprint 1.6 m/s and bonuses +2 and +1.5, the additional sprint component is 5.1 m/s. Their sum is 16.5 m/s, not measured velocity.
V1 supports at most one nonzero movement percentage. That percentage affects movement, including flat adjustments, but not the sprint component. Multiple percentages or negative final speeds stay unresolved. Values do not simulate movement states; state queries report those states in another query.
Catalog inputs and coverage¶
Bases use EMaxMoveSpeed and ESprintSpeed, with catalog hero spirit scaling.
Flat effects use MODIFIER_VALUE_MOVEMENT_SPEED_MAX and MODIFIER_VALUE_SPRINT_SPEED_BONUS.
Percentages use MODIFIER_VALUE_MOVEMENT_SPEED_MAX_PERCENT.
Bare numeric modifier amounts use Source units/s; metric properties use m/s.
Boon converts upgrade amounts to the same units.
Trophy Collector uses a supplied catalog binding to m_iTrophyCount.
Missing bindings, intrinsic modifiers, counts, or scaling can prevent a complete value.
See movement limits.
Debuff resistance¶
debuff_resist = 100 * (1 - product(1 - source_percent / 100))
affected_duration = original_duration * (1 - debuff_resist / 100)
The base uses EDebuffResist; effects use MODIFIER_VALUE_STATUS_RESISTANCE.
A missing base contributes zero. Negative resistance increases duration.
Two 25% sources give 43.75%. A 30% result reduces ten seconds to seven seconds.
The result does not identify immunity, cleansing, or debuffs that ignore resistance.
Damage resistance¶
innate = base + boon growth + spirit scaling
resistance = 100 * (1 - (1 - innate / 100) * product(1 - other_source / 100))
reduction = 100 * (1 - product(1 - reduction_source / 100))
result = resistance - reduction
Stat |
Hero base |
Resistance modifier |
Reduction modifier |
|---|---|---|---|
|
|
|
|
|
|
|
|
|
|
|
|
For resistance sources 40% and 20%, the result before reductions is 52%.
Reductions of 25% and 20% combine to 40%, which leaves 12% resistance.
A missing base is zero. Negative final values stay negative.
Reductions apply to the recipient. Property upgrades use the caster’s state.
Catalog reductions use negative values; unsupported positive reductions stay unresolved.
melee_resist reports only the melee component. Shared bullet/melee resistance stays in bullet_resist.
Lifesteal¶
lifesteal = 100 * (1 - product(1 - source_percent / 100))
Each damage type has its own calculation. Source percentages must be finite and between 0 and 100.
For 22% and 30%, the result is 45.4%.
Bullet bases use EBulletLifesteal; spirit bases use ETechLifesteal.
Effects use MODIFIER_VALUE_BULLET_LIFESTEAL and MODIFIER_VALUE_TECH_LIFESTEAL.
A missing base is zero.
Melee uses the exact MeleeLifesteal property on an owned passive ability, with its upgrades.
This assumed property link gives a partial value.
Target-dependent TargetLifesteal stays unresolved.
Cooldown-based melee healing items are not included.
Results do not include healing adjustments and creep effectiveness; they do not predict health gained.
Unresolved inputs¶
Status |
Meaning |
|---|---|
|
The rule calculated a value from supported inputs. |
|
The value has missing effects or an assumed link. |
|
The value is null because the rule cannot calculate it. |
Unknown modifiers and assumed activation links give partial values with either strict setting.
Other missing inputs raise CalculationError with strict=True.
With strict=False, those rows have null values and diagnostics.
Invalid queries, missing ticks, and download failures still cause errors.
Even a calculated value cannot include effects missing from the catalog.
Boon ignores the two known ping markers for stat calculations. They stay in modifier datasets. See Known Issues for binding, scaling, and lifetime limits.
Rust¶
Rust queries use HeroStat enum members and u64 Steam IDs.
use boon::{
Parser,
hero_stats::{HeroStat, HeroStatQuery, Ruleset, StatCatalog, StatMode},
rulesets,
};
use std::path::Path;
fn main() -> Result<(), Box<dyn std::error::Error>> {
let parser = Parser::from_file(Path::new("108575009.dem"))?;
let catalog = StatCatalog::load("6712")?;
let query = HeroStatQuery::new(
[187554, 187600],
[HeroStat::ClipSize, HeroStat::FireRate],
)
.steam_ids([76561198037652386])
.mode(StatMode::Current)
.explain(true)
.strict(false);
let rules = Ruleset::new()
.with(rulesets::clip_size::V1)
.with(rulesets::fire_rate::V1);
let result = parser.calculate_hero_stats(&query, &catalog, &rules)?;
println!("{:?}", result.values);
Ok(())
}
Reuse StatCatalog across queries. StatCatalog::load uses the verified cache
from boon get. StatCatalog::from_directory reads local files without downloads
or checksum checks.