Add per-minute hero damage, healing and camp stack series - #87
Conversation
Emit camps_stacked_t, hero_damage_t and hero_healing_t arrays per player, sampled on minute boundaries like gold_t/lh_t/xp_t/dn_t. - camps_stacked_t mirrors the cumulative camps_stacked interval value; left empty for old replays without camps_stacked interval data - hero_damage_t follows the hero_damage scoreboard definition, verified empirically against a real match: damage dealt to real (non-illusion) heroes, self-damage excluded, illusion attacker damage counted toward the owning hero - hero_healing_t follows the hero_healing scoreboard definition: healing done to heroes other than yourself Damage/heal events are bucketed by event time in a pre-scan rather than accumulated in stream order: combat log entries can appear in the stream after the interval entry of the same game time, which would drop the final teamfight of a match from the last sample. Implemented in the Java processors (the live path) and mirrored in the legacy JS reference processors.
|
This will increase the storage cost for future matches--do we have a specific need for it? |
| }, | ||
| times: [], | ||
| gold_t: [], | ||
| lh_t: [], |
There was a problem hiding this comment.
We've since switched to the Java implementations so these are just here for reference.
There was a problem hiding this comment.
Yep, that's why the primary implementation here is in CreateParsedDataBlob.java — I updated the .mjs alongside it just to keep the reference in sync with what the Java actually does. Happy to drop the JS changes if you'd rather keep the diff minimal.
There was a problem hiding this comment.
Done - dropped the .mjs changes in 936e5de, the diff now only touches the Java path.
|
Measured master vs this branch on the same replays. The blob archive stores gzip, so that's the relevant column:
So roughly +0.6–0.9 KB gzipped per parsed match (~2–3% of the blob) — the arrays compress well since they're monotonic integers with long flat stretches. Example output (the 18-min match, Dazzle): "times": [0, 60, 120, 180, ...],
"hero_damage_t": [0,158,713,1126,1201,1536,1846,1846,2731,2731,2886,2886,2988,3170,3170,3170,3170,3207,3207],
"hero_healing_t": [0,0,85,170,400,525,1019,1353,1788,1933,2078,2078,2223,2458,2501,2501,2501,3076,3076],
"camps_stacked_t": [0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0]On the specific need: I'm building a coaching tool on top of the OpenDota API that gives per-phase feedback (laning / mid game / late game). Damage output, healing and stacking are core coaching signals for supports and mids, and today the API only supports time-phased analysis for gold/xp/cs — for everything else I'd have to download and reparse replays myself, or use Stratz, which exposes exactly these fields ( If storage is still a concern, I'm happy to drop |
The JS processors are reference-only since the switch to the Java implementation, so the new series no longer need to be mirrored there.
| // pre-scan (rather than accumulated in stream order) because combat log | ||
| // entries can appear in the stream after the interval entry of the same | ||
| // game time, which would drop the final minute of a match. | ||
| private Map<Integer, Map<Integer, Integer>> heroDamageMinuteBySlot = new HashMap<>(); |
There was a problem hiding this comment.
Why do we need to bucket into minutes here? Isn't hero damage/healing additive, so we can just add to it whenever we encounter a combat log event and then add on minute boundaries?
This adds three new per-player arrays to the parsed data blob, sampled on minute boundaries exactly like the existing
gold_t/lh_t/xp_t/dn_tseries:camps_stacked_t— cumulative camps stacked at each minute. The value already exists on every interval entry; this just samples it into a series. Left empty for old replays that don't carrycamps_stackedin interval data.hero_damage_t— cumulative damage dealt to enemy heroes at each minute, accumulated fromDOTA_COMBATLOG_DAMAGE. Follows the Valvehero_damagescoreboard definition (verified empirically): real non-illusion hero targets, self-damage excluded, illusion attacker damage counted toward the owning hero. The final element matches the scoreboardhero_damageexactly, up to damage dealt after the last minute boundary.hero_healing_t— cumulative healing done to other heroes at each minute, accumulated fromDOTA_COMBATLOG_HEAL. Follows the Valvehero_healingscoreboard definition (verified empirically): hero targets excluding self-healing.Implementation note: damage/heal events are bucketed by event time in a pre-scan instead of accumulated in stream order — combat log entries can appear in the stream after the interval entry for the same game time (notably the final teamfight of a match), which would silently drop the last minute of damage.
Motivation: these series enable time-phased analysis (laning / mid / late) of damage output, healing and stacking, which currently is only possible for gold/xp/cs. They close one of the data gaps relative to Stratz's
heroDamagePerMinute/healPerMinute/campStackfields, and pair with a small odota/core PR that documents the new fields in the OpenAPI spec. Related demand: odota/web#1834 asks for damage-per-minute display, including "DPM at the specific time interval" — these series make that possible.The implementation lives in the Java port (
CreateParsedDataBlob.java), which is the live path since "implement processors in Java"; the legacy JS processors (processExpand.mjs/parseSchema.mjs) are updated in the same way to keep the reference implementation in sync.No schema version bump (additive fields only, same approach as the pause timings addition). Old parsed blobs simply won't have the fields.
Testing:
damage/healingdicts unchanged.odota.github.io/testfiles/1781962623_1.dem) parsed via/blob?replay_url=and diffed against the reference blob checked into odota/core (json/1781962623_parsed.json) — all pre-existing fields byte-identical;camps_stacked_tcorrectly empty (old replay).hero_damage_tfinal element matches the Valve scoreboardhero_damageexactly for 9/10 players (the 10th is 0.999 — damage dealt after the last minute boundary, by design, same asgold_tvs total gold);hero_healing_tfinal matcheshero_healingexactly for 9/10 (one off by 208 out of 4111);camps_stacked_tfinal matchescamps_stackedfor 10/10.Performance: benchmarked master vs this branch on the same real replay (18 min match), 9 measured runs each after a JIT warmup run, comparing the server-side
parse:stderr timing (CPU work only — download and decompression excluded):The delta (~2% on the min; medians overlap) is below the ±10% run-to-run variance. The added work is one extra O(n) pre-scan over the entries plus three interval entries per player per minute, while total parse time is dominated by the
.demdecode. Blob size grows by a few KB on a ~150 KB blob.