From e17d7dff04647b296657f3bc97104625ffbe7efe Mon Sep 17 00:00:00 2001 From: grom Date: Thu, 29 Oct 2026 08:44:25 -0600 Subject: [PATCH] The slam-clip macro chain is sound, and now has a test that says so MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit His rotation macro never picks Slam. Five of our verbs and feeds stand between `[noslamclip]` and an answer, and any one of them would read in play exactly as he describes, so this drives all five on his own 92-addon interface rather than reasoning about them: UnitAttackSpeed's (number, nil) shape, which is what isDualWield() asks; CHAT_MSG_COMBAT_SELF_HITS reaching a registered frame with arg1 as the composed sentence, which is the only thing SP_SwingTimer resets on; the GetSpellName spellbook walk; GameTooltip:SetSpell on a nil-parent scanning tooltip built from GameTooltipTemplate, read back through getglobal; and the delta arriving as seconds. All five answer correctly. Measured, the conditional is true at the top of a swing with a 44.4% window off a scanned 1.5 s cast time — so the cause is not in this chain, and the test is here to keep that true. The one asymmetry it does pin down is faithful: before any swing lands, SP_SwingTimer's ADDON_LOADED arm latches st_timerMax to 0, because UnitAttackSpeed answers 0 while the stats feed has not pushed and 0 is truthy in Lua, so its own `if not st_timerMax` guard never fires again. reference/1.12-shapes.tsv has UnitAttackSpeed returning exactly two values and never none, so answering 0 is right; the first swing heals it, which the second half of the test shows. The scanning-tooltip leg is worth naming on its own: without it the addon falls back to a 2.5 s constant, and against a 2.70 s swing that is a 7% window instead of 44% — which would read as "Slam never fires" too. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_01SgstFWtn1RxEJ5kVrpgYwn --- .../src/addon_harness/tick_bench_tests.rs | 165 ++++++++++++++++++ 1 file changed, 165 insertions(+) diff --git a/crates/benilla-app/src/addon_harness/tick_bench_tests.rs b/crates/benilla-app/src/addon_harness/tick_bench_tests.rs index 8080c53e..ba9b4d67 100644 --- a/crates/benilla-app/src/addon_harness/tick_bench_tests.rs +++ b/crates/benilla-app/src/addon_harness/tick_bench_tests.rs @@ -1495,3 +1495,168 @@ fn a_cursor_seated_sign_plate_prices_its_own_re_anchor() { ); } } + +/// SuperCleveRoidMacros' `[noslamclip]` conditional, end to end, on grom's own interface. +/// +/// He reports that `/cast [noslamclip, stance:1/3] Slam; [slamclip, stance:1/3] Heroic Strike` +/// never picks Slam. This drives the whole chain the conditional stands on, because every link in +/// it is a verb or a feed of ours and any one of them could be the cause: +/// +/// * `UnitAttackSpeed("player")` — SP_SwingTimer's `GetWeaponSpeed`, and the `(number, nil)` +/// shape for a one-handed weapon that `isDualWield()` keys on. +/// * `CHAT_MSG_COMBAT_SELF_HITS` reaching a registered frame with `arg1` as the composed English +/// sentence. SP_SwingTimer resets its timer on nothing else; `ui_chat::frames::route` is the +/// one function that both tees a line to `WoWCombatLog.txt` and fires the event, so a line in +/// that file means this fire happened. +/// * The spellbook walk `GetSpellName(i, BOOKTYPE_SPELL)` finding Slam. +/// * `GameTooltip:SetSpell` on a **nil-parent scanning tooltip built from +/// `GameTooltipTemplate`**, whose `TextLeft` the addon reads with `getglobal` looking +/// for `"%d+%.?%d* sec cast"`. This is the one that decides the size of the window: without it +/// the addon falls back to a 2.5 s constant, which against a 2.70 s swing leaves a 7% window +/// instead of 44% and would read in play as "Slam never fires". +/// * The `` delta reaching `SP_ST_OnUpdate(arg1)` as seconds. +/// +/// **All five answer correctly**, which is the finding: measured here the conditional is `true` +/// at the top of a swing, with a 44.4% window off a 1.5 s scanned cast time. So whatever he is +/// seeing is not one of these, and this test exists so that stays true. +/// +/// The one real asymmetry it documents is the first bullet's edge: before any swing has landed, +/// `st_timerMax` is latched to `0` by SP_SwingTimer's `ADDON_LOADED` arm, because +/// `UnitAttackSpeed("player")` answers `0` while the stats feed has not pushed yet — and `0` is +/// truthy in Lua, so its own `if not st_timerMax` guard never re-fires. That is *faithful* +/// (`reference/1.12-shapes.tsv` has `UnitAttackSpeed` returning exactly 2 values, never none), and +/// it heals on the first swing, as the second half of this test shows. +#[test] +fn the_slam_clip_conditional_answers_on_the_players_own_interface() { + let _data = benilla_formats::wow_data_or_skip!(); + let Some(root) = crate::ui_script::addons::root() else { + eprintln!("slam-clip probe: no benilla-config/AddOns — nothing to ask"); + return; + }; + let mut s = load_the_players_ui(&root); + // What the live feeds push and this loader does not: his own sheet (2.70 s mainhand, no + // offhand), Slam in the spellbook with its tooltip, and a target to be hitting. + s.set_player_combat_stats(Some(benilla_ui::script::UnitCombatStats { + main_attack_time_ms: 2700, + has_offhand: false, + min_damage: 41.0, + max_damage: 51.0, + ..Default::default() + })); + s.set_spell_tooltip( + 1464, + benilla_ui::script::SpellTooltipView { + name: "Slam".into(), + rank: Some("Rank 1".into()), + cost: Some("15 Rage".into()), + range: Some("Melee Range".into()), + cast_time: Some("1.5 sec cast".into()), + description: "Slams the opponent.".into(), + ..Default::default() + }, + ); + s.set_spellbook(benilla_ui::script::SpellBookState { + tabs: Vec::new(), + slots: vec![benilla_ui::script::SpellSlotView { + spell_id: 1464, + name: "Slam".into(), + ..Default::default() + }], + }); + s.set_unit( + "target", + Some(UnitState { + exists: true, + has_object: true, + name: Some("Apprentice Training Dummy".into()), + health: 100, + max_health: 100, + level: 14, + guid: 0x7001, + ..Default::default() + }), + ); + + let ask = |s: &mut UiScript, chunk: &str| -> String { + s.eval::(&format!( + "local ok, v = pcall(function() return {chunk} end) return tostring(v)" + )) + .unwrap_or_else(|e| format!("<{e}>")) + }; + + // The addon's own swing timer is registered for the event and answerable before any of it. + assert_eq!( + ask( + &mut s, + "SP_ST_Updater:IsEventRegistered('CHAT_MSG_COMBAT_SELF_HITS')" + ), + "1", + "SP_SwingTimer's updater frame did not register the event it resets on" + ); + assert_eq!( + ask(&mut s, "(UnitAttackSpeed('player'))"), + "2.7", + "the sheet's mainhand speed, which is the swing the window is a fraction of" + ); + assert_eq!( + ask(&mut s, "select(2, UnitAttackSpeed('player'))"), + "nil", + "no offhand is `nil`, not 0 — `isDualWield()` is exactly this test" + ); + // The scanning tooltip: a nil parent, the shipped template, and `getglobal` down the lines. + assert_eq!( + ask(&mut s, "CleveRoids.GetSlamCastTime()"), + "1.5", + "the cast time came off the scanning tooltip, not the addon's 2.5 s fallback — the \ + fallback leaves a 7% window against a 2.70 s swing and reads as `[noslamclip]` never \ + being true" + ); + // The latch, before any swing: documented, faithful, and healed below. + assert_eq!( + ask(&mut s, "st_timerMax"), + "0", + "SP_SwingTimer's ADDON_LOADED arm latched the pre-feed `UnitAttackSpeed` answer" + ); + + // One auto-attack lands, exactly as `ui_chat::frames::route` fires it. + s.fire_event( + "CHAT_MSG_COMBAT_SELF_HITS", + vec![benilla_ui::script::ScriptValue::Str( + "You hit Apprentice Training Dummy for 46.".into(), + )], + ); + assert_eq!( + ask(&mut s, "st_timerMax"), + "2.7", + "the swing reached SP_ST_OnEvent with a parsable `arg1` and reset the timer" + ); + assert_eq!( + ask(&mut s, "CleveRoids.GetSwingPercentElapsed()"), + "0", + "at the top of a swing, none of it has elapsed" + ); + assert_eq!( + ask(&mut s, "CleveRoids.GetSlamWindowPercent()"), + "44.444444444444", + "(2.70 - 1.50) / 2.70 — the share of the swing in which Slam still beats the auto-attack" + ); + assert_eq!( + ask(&mut s, "CleveRoids.ValidateNoSlamClip()"), + "true", + "**the conditional itself**: at the top of a swing `[noslamclip]` is true, so the macro's \ + first clause is the one that should fire" + ); + + // The `` delta: the timer has to run down in real seconds, or the window never + // closes and `[slamclip]` never takes over. + for _ in 0..6 { + s.tick(1.0 / 60.0); + } + let left: f64 = s.eval("return st_timer").expect("st_timer"); + assert!( + (left - (2.7 - 6.0 / 60.0)).abs() < 1e-3, + "six frames of 1/60 s should leave {:.4} s on the swing, not {left:.4} — \ + `SP_ST_OnUpdate(arg1)` is not getting the frame's elapsed seconds", + 2.7 - 6.0 / 60.0, + ); +}