The slam-clip macro chain is sound, and now has a test that says so
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 <OnUpdate> 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 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01SgstFWtn1RxEJ5kVrpgYwn
This commit is contained in:
@@ -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 `<name>TextLeft<i>` 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 `<OnUpdate>` 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::<String>(&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 `<OnUpdate>` 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,
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user