v1.4.1 — cactbot Stage 2.1 polish (multi-cast assignment + comment-block fallback names)
- `timeline_diff_for_fight` previously had each fight_model row pick the closest cast independently — when ability X had N rows and M casts, multiple rows could pick the same cast…
Fixed — multi-cast assignment collision
timeline_diff_for_fightpreviously had each fight_model row pick the closest cast independently — when ability X had N rows and M casts, multiple rows could pick the same cast or distant casts. Now per-ability matching: sort rows by expected time, then for each row pick the nearest unused cast and consume it. Optimal for the common case of monotonic timelines; degrades gracefully when there are more rows than casts (extras marked missing).- 2 new regression tests in
tests/test_timeline_diff.py:test_multi_cast_no_collision,test_multi_cast_extra_rows_marked_missing.
Fixed — adds-phase / sub-cast abilities now get human labels
- Cactbot timelines document many abilities in two places the old parser missed:
- Trailing
# <HEX> <Name>comment block at the bottom of the file (sub-cast VFX, etc.). Used by FRU + Heavyweight tier. - Commented-out
<time> "<label>" #Ability { id: ... }lines scattered through the file body (sub-cast effects with labels). Used heavily by TOP / DSR.
- Trailing
- Parser now harvests both into
ParsedTimeline.fallback_names: dict[ability_id, str]. The annotator uses fallback names when no timeline-body entry matches. Fallback annotations get a label but nocactbot_expected_t_ms(no firm expected timing). - New result counter:
annotated_fallback. UI / API consumers seecactbot_labelpopulated where it was previously null. - Loosened the
#Abilityregex to accept<time> "<label>" duration N #Ability {...}(TOP-style) by allowing optional intermediate tokens between the closing quote and the#Abilitymarker. - 4 new tests in
tests/test_cactbot.py:test_fallback_names_parsed_from_comment_block,test_real_fru_comment_block_picks_up_hiemal_ray,test_commented_ability_lines_become_fallback_names,test_annotate_uses_fallback_name_when_no_body_entry.
Live AC — annotation coverage after the fixes
| Encounter | Before (1.3.0) | After (1.4.1) | Δ |
|---|---|---|---|
| M9S | 38% (16/42) | 93% (39/42) | +55pp |
| M10S | 52% (32/62) | 94% (58/62) | +42pp |
| M11S | 63% (52/83) | 94% (78/83) | +31pp |
| M12S | 50% (28/56) | 84% (47/56) | +34pp |
| M12S-P2 | 62% (45/72) | 93% (67/72) | +31pp |
| FRU | 55% (79/144) | 94% (135/144) | +39pp |
| TOP | 48% (63/132) | 56% (74/132) | +8pp (cosmetic-heavy remainder) |
| DSR | 46% (75/162) | 72% (116/162) | +26pp |
340 tests passing (334 → 340, +6).
Known limitation still standing (not in scope of this fix)
- Multi-ID slot mapping. When cactbot writes
Ability { id: ["X", "Y"] }(random-variant mechanics like Sinsmoke/Sinsmite), our fight_model has separate rows per ability_id. A pull where the "Y" variant fired first leaves the "X" row hunting the second-position cast, producing misleading drift like the FRU Sinsmoke +42.8s case. Fixing requires structural changes (slot-level matching where one cactbot slot maps to N possible fight_model rows). Documented for later.