Vigil
← All news

v1.16.3 — Cartography uses inference for non-attributable deaths

v1.16.1 wired non-attributable-death inference (cast proximity + cactbot drift) into `compute_fault_scores_for_fight`, but the cartography "What's killing us" view read death even…

Why

v1.16.1 wired non-attributable-death inference (cast proximity + cactbot drift) into compute_fault_scores_for_fight, but the cartography "What's killing us" view read death events raw — so FFLogs-sourceID=-1 deaths still lumped into a single bucket and usually dominated the chart. User asked: "Can we fix non-attributable by attributing it to the closest cactbot mechanic? Just label it as guessed or something."

Added — analysis/death_inference.py

Shared inference module pulled out of fault_attribution. Exports:

  • INFER_LOOKBACK_MS = 8_000 / INFER_CACTBOT_TOLERANCE_MS = 2_500 / INFER_ACTIONABLE_LABELS
  • infer_killer_from_cast_proximity(death_ts, enemy_casts, label_of) — most recent enemy cast within 8s whose type_label is actionable
  • infer_killer_from_cactbot_drift(death_ts, phase, phase_start, cactbot_entries, drift, label_of) — drift-adjusted cactbot lookup, ±2.5s
  • build_phase_drift_map(timeline_diff_result) — phase_index → median_drift_ms
  • boss_cast_events(session, fight_id) — enemy-sourced casts for one fight
  • build_inference_context(session, fight_id, encounter_id) — pre-loads everything needed for infer_killer
  • infer_killer(ctx, death_ts) → (aid, label, source) — two-layer pipeline: cast_proximity first, cactbot_drift fallback

fault_attribution re-exports the prior private names (_infer_killer_from_cast_proximity, etc.) so existing tests keep working.

Changed — cartography_for_encounter uses inference

analysis/cartography.py. For each death with ability_id IS NULL, build a per-fight inference context (lazy-loaded only for fights that actually have non-attributable deaths) and call infer_killer. If a match is found, increment that ability's bucket and add to inferred_deaths. Otherwise the death stays in the non-attributable bucket.

Per-bucket response gained inferred_deaths: int — how many of this row's deaths came from inference (vs real FFLogs attribution).

UI — "(guessed)" pill on cartography rows

web/src/Home.jsx. When a mechanic row has inferred_deaths > 0, a yellow pill appears next to the mechanic name: just guessed if every death in the bucket was inferred, or N guessed showing how many of the total were guesses. Tooltip explains the inference source.

Tests

2 new in tests/test_cartography_inference_v1_16_3.py: cast-proximity attribution end-to-end + fallback to non-attributable bucket when inference can't match. 513 tests passing (511 → 513).

Live AC against DSR data

Top mechanic table before this ship had non-attributable as a giant lump at the top. After:

  • Sacred Sever: 156 deaths (70 guessed, recovered from non-attributable)
  • Holy Bladedance: 106 deaths (80 guessed)
  • Ancient Quaga: 84 deaths (72 guessed)
  • Heavensflame: 105 deaths (31 guessed)

Non-attributable residual: 615 deaths. These are real "we can't tell" cases — sub-cast VFX outside both our events table and cactbot's timeline body. Future improvement: harvest cactbot's commented-out #Ability entries (already done for label fallback in v1.4.1) into the drift inference too.