Implement ISBoxer-style smart interact shortcut (bind: interact)
A step with bind: interact now expands at compile time into the full chain
(CTM on -> Interact with Target -> sleep walk_delay_ms -> CTM off), driven
by profile.interact (style + walk_delay_ms). One user keypress, four
keystrokes dispatched to every captured slot.
Styles:
- standard (default): chain runs, CTM ends off
- auto: chain runs, CTM stays on (toggle via ctm_off later)
- hold: press fires CTM-on + Alt+J; bind: ctm_off in
release_steps to fire it on hotkey release
Example loot (Alt+G) and interact (Alt+I) maps now use the shortcut. A
loot_manual map (Ctrl+Alt+G) exercises the explicit chain for users who
want per-step control.
92/92 unit tests pass; clippy clean. New tests:
- interact_smart_shortcut_standard_emits_full_sequence
- interact_smart_shortcut_auto_emits_two_no_tail
- interact_smart_shortcut_hold_emits_press_only
Docs: MACROS.md (new Smart interact shortcut section with style table),
NOTES.md callout, examples/profile.yaml comments, CHANGELOG.md entry.
This commit is contained in:
parent
aeed2036d0
commit
1ec8d78f71
21
CHANGELOG.md
21
CHANGELOG.md
@ -27,3 +27,24 @@
|
||||
- T15: Teams + Lutris launcher. `src/team.rs` (Team, list_teams, teams_dir, current_team), `src/lutris.rs` (LutrisGame parser, `load_all` with bad-YAML tolerance), `src/launcher.rs` (SpawnPlan merging Lutris config with per-character wine-prefix override). GUI: Teams menu (New / Switch / Refresh / Show / Delete) and Launch menu (per-character + Launch all); Lutris picker visible only in the New-team flow. Direct Wine spawn — never calls `lutris` CLI
|
||||
- `game_binds.interact` example uses **Alt+J** (Master's request). Interact with Target is a built-in WoW keybind; enBoxer just routes the keystroke.
|
||||
- `docs/MACROS.md` rewritten for current retail (The War Within era, 2026): rename "CTM" → Auto-Interact (the CVar), document Alt+J for Interact with Target, document per-character Assist / Follow macro override, add optional Pet Attack / Target Last Target keybinds.
|
||||
|
||||
## Unreleased — Interact chain (ISBoxer-style mapped key)
|
||||
|
||||
- **Smart interact shortcut.** A step with `bind: interact` now expands at
|
||||
compile time into the full CTM-on → Interact-with-Target → sleep
|
||||
`walk_delay_ms` → CTM-off chain, driven by `profile.interact` (`style`
|
||||
+ `walk_delay_ms`). One user keypress, four keystrokes dispatched to
|
||||
every captured slot. Styles:
|
||||
- `standard` (default): chain runs, CTM ends off.
|
||||
- `auto`: chain runs, CTM stays on (toggle via `ctm_off` later).
|
||||
- `hold`: press fires CTM-on + Alt+J; put `bind: ctm_off` in
|
||||
`release_steps` to fire it on hotkey release.
|
||||
- The example `loot` map (Alt+G) and `interact` map (Alt+I) now use the
|
||||
shortcut. A `loot_manual` map (Ctrl+Alt+G) exercises the explicit
|
||||
chain for users who want per-step control.
|
||||
- New tests: `interact_smart_shortcut_standard_emits_full_sequence`,
|
||||
`interact_smart_shortcut_auto_emits_two_no_tail`,
|
||||
`interact_smart_shortcut_hold_emits_press_only`. 92/92 unit tests pass;
|
||||
clippy clean.
|
||||
- Docs: `docs/MACROS.md` "Smart interact shortcut" section, `docs/NOTES.md`
|
||||
callout, `examples/profile.yaml` updated with comments.
|
||||
|
||||
@ -77,3 +77,45 @@ Both are pure in-game keybinds; enBoxer just routes the keystroke.
|
||||
Do not mash. A second interact while they are still pathing is how characters run circles.
|
||||
|
||||
If they stop short, raise `walk_delay_ms` (2500–4000), or use `interact.style: auto` (CTM stays on), or `interact.style: hold`.
|
||||
|
||||
## Smart interact shortcut (`bind: interact`)
|
||||
|
||||
The example profile's `loot` map (Alt+G) and `interact` map (Alt+I) both use a **smart shortcut**: a single user keypress fires the full chain (CTM on → Interact with Target → sleep `walk_delay_ms` → CTM off). It is what ISBoxer did with a "Mapped Key" — you press one key, the multibox software sends the whole chain.
|
||||
|
||||
In enBoxer the shortcut is `bind: interact` in a step:
|
||||
|
||||
```yaml
|
||||
- name: loot
|
||||
hotkey: "Alt+G"
|
||||
steps:
|
||||
- bind: assist
|
||||
target: others
|
||||
- bind: interact # <-- smart shortcut
|
||||
target: others
|
||||
```
|
||||
|
||||
The engine expands that one step at compile time. Driven by `profile.interact`:
|
||||
|
||||
| `interact.style` | What happens on the press | CTM after the press |
|
||||
| ---------------- | ----------------------------------------------------------------------------------------- | --------------------- |
|
||||
| `standard` | `ctm_on` → `interact` (`game_binds.interact`) → sleep `walk_delay_ms` → `ctm_off` | off |
|
||||
| `auto` | `ctm_on` → `interact` | stays on (toggle off via `ctm_off` when you want) |
|
||||
| `hold` | `ctm_on` → `interact` (Down); put `bind: ctm_off` in `release_steps` to turn it off on release | matches the hotkey down/up |
|
||||
|
||||
Walk distance and follower latency are absorbed by `walk_delay_ms` (default 2500 ms). If your alts stop short, raise it (2500–5000 ms is typical for indoor encounters).
|
||||
|
||||
If you need per-step control (different keys, branches, conditional delays), use the manual form:
|
||||
|
||||
```yaml
|
||||
- name: loot_manual
|
||||
hotkey: "Ctrl+Alt+G"
|
||||
steps:
|
||||
- bind: assist
|
||||
- bind: ctm_on
|
||||
- bind: interact
|
||||
- delay_ms: 5000
|
||||
- bind: ctm_off
|
||||
```
|
||||
|
||||
Both forms are tested in `engine::tests`; see `interact_smart_shortcut_standard_emits_full_sequence`, `…_auto_emits_two_no_tail`, `…_hold_emits_press_only`, and `loot_manual` (which exercises the explicit chain).
|
||||
|
||||
|
||||
@ -37,3 +37,11 @@ If a window is **native Wayland** (including `winewayland`), unfocused inject of
|
||||
The Session page lists each window as `XWayland/Wine` or `Wayland`.
|
||||
|
||||
Visible-pixel capture: `grim` (default). Overlay: `mpv --wayland-app-id=enboxer-vfx` plus JSON IPC to reload frames. Covered sources use `zwlr_export_dmabuf_unstable_v1` (Hyprland implements the wlroots protocol); gated behind `ENBOXER_ENABLE_TOPLEVEL=1`. See [docs/VIDEO.md](VIDEO.md).
|
||||
|
||||
## Smart interact shortcut
|
||||
|
||||
`bind: interact` in any map step expands at compile time into the full chain
|
||||
(CTM on → Interact with Target → walk delay → CTM off), driven by
|
||||
`profile.interact` (`style` + `walk_delay_ms`). This is the ISBoxer Mapped
|
||||
Key analog — one user keypress, four keystrokes dispatched to every captured
|
||||
slot. See `docs/MACROS.md` for the per-style table and the manual form.
|
||||
|
||||
@ -106,7 +106,22 @@ maps:
|
||||
target: others
|
||||
|
||||
- name: loot
|
||||
# ISBoxer Mapped Key analog: one user keypress fires the entire interact
|
||||
# chain (CTM on -> Interact with Target -> wait walk_delay_ms -> CTM off),
|
||||
# driven by profile.interact (style + walk_delay_ms). The follower has
|
||||
# walk_delay_ms to actually walk to the target before CTM turns off.
|
||||
hotkey: "Alt+G"
|
||||
steps:
|
||||
- bind: assist
|
||||
target: others
|
||||
- bind: interact
|
||||
target: others
|
||||
- name: loot_manual
|
||||
# Same idea as `loot` but composed by hand. Pick this form if you want to
|
||||
# do anything fancy between the CTM toggle and the interact (e.g. a delay
|
||||
# specific to a slow class, or a different key for each step).
|
||||
hotkey: "Ctrl+Alt+G"
|
||||
hold: false
|
||||
steps:
|
||||
- bind: assist
|
||||
target: others
|
||||
@ -114,10 +129,11 @@ maps:
|
||||
target: others
|
||||
- bind: interact
|
||||
target: others
|
||||
- delay_ms: 2500
|
||||
- delay_ms: 5000
|
||||
- bind: ctm_off
|
||||
target: others
|
||||
|
||||
|
||||
- name: interact_hold
|
||||
hotkey: "Alt+H"
|
||||
hold: true
|
||||
|
||||
206
src/engine.rs
206
src/engine.rs
@ -205,6 +205,72 @@ impl Engine {
|
||||
out.push(Action::Sleep(Duration::from_millis(ms)));
|
||||
continue;
|
||||
}
|
||||
// ISBoxer-style Mapped Key shortcut: `bind: "interact"` fires the
|
||||
// full CTM-toggle + Interact-with-Target + (optional) wait + CTM-off
|
||||
// chain driven by profile.interact (style + walk_delay_ms).
|
||||
// The user only pressed the map hotkey; the daemon composes the chain.
|
||||
if step.bind.as_deref() == Some("interact") {
|
||||
let slots = self.resolve_targets(&step.target, &map_name)?;
|
||||
let interact_key = self
|
||||
.profile
|
||||
.game_binds
|
||||
.get("interact")
|
||||
.cloned()
|
||||
.unwrap_or_default();
|
||||
let ctm_on = self
|
||||
.profile
|
||||
.game_binds
|
||||
.get("ctm_on")
|
||||
.cloned()
|
||||
.unwrap_or_default();
|
||||
let ctm_off = self
|
||||
.profile
|
||||
.game_binds
|
||||
.get("ctm_off")
|
||||
.cloned()
|
||||
.unwrap_or_default();
|
||||
|
||||
// 1. CTM on (sends the macro key that toggles autoInteract on).
|
||||
if !ctm_on.is_empty() {
|
||||
out.push(Action::Send {
|
||||
key: ctm_on,
|
||||
slots: slots.clone(),
|
||||
hold,
|
||||
});
|
||||
}
|
||||
// 2. Interact with Target keybind.
|
||||
if !interact_key.is_empty() {
|
||||
out.push(Action::Send {
|
||||
key: interact_key,
|
||||
slots: slots.clone(),
|
||||
hold,
|
||||
});
|
||||
}
|
||||
// 3. Style-driven tail.
|
||||
match self.profile.interact.style {
|
||||
crate::profile::InteractStyle::Standard => {
|
||||
out.push(Action::Sleep(Duration::from_millis(
|
||||
self.profile.interact.walk_delay_ms,
|
||||
)));
|
||||
if !ctm_off.is_empty() {
|
||||
out.push(Action::Send {
|
||||
key: ctm_off,
|
||||
slots,
|
||||
hold,
|
||||
});
|
||||
}
|
||||
}
|
||||
crate::profile::InteractStyle::Auto => {
|
||||
// CTM stays on after the press. The user toggles it off
|
||||
// (via game_binds.ctm_off) when they explicitly want to.
|
||||
}
|
||||
crate::profile::InteractStyle::Hold => {
|
||||
// CTM is bound on the press; ctm_off goes on release_steps.
|
||||
// No tail here.
|
||||
}
|
||||
}
|
||||
continue;
|
||||
}
|
||||
let key = if let Some(k) = &step.key {
|
||||
k.clone()
|
||||
} else if let Some(b) = &step.bind {
|
||||
@ -273,9 +339,34 @@ mod tests {
|
||||
release_steps: vec![],
|
||||
},
|
||||
Map {
|
||||
// Smart shortcut: one user keypress fires the entire interact
|
||||
// chain (CTM on -> Alt+J -> sleep walk_delay_ms -> CTM off).
|
||||
name: "loot".into(),
|
||||
hotkey: Hotkey("Alt+G".into()),
|
||||
hold: false,
|
||||
steps: vec![
|
||||
Step {
|
||||
bind: Some("assist".into()),
|
||||
key: None,
|
||||
delay_ms: None,
|
||||
target: "others".into(),
|
||||
},
|
||||
Step {
|
||||
bind: Some("interact".into()),
|
||||
key: None,
|
||||
delay_ms: None,
|
||||
target: "others".into(),
|
||||
},
|
||||
],
|
||||
release_steps: vec![],
|
||||
},
|
||||
// Manual form: same chain composed by hand. Use this when you
|
||||
// need per-step control (different key per step, custom delay
|
||||
// between, conditional branches, etc.).
|
||||
Map {
|
||||
name: "loot_manual".into(),
|
||||
hotkey: Hotkey("Ctrl+Alt+G".into()),
|
||||
hold: false,
|
||||
steps: vec![
|
||||
Step {
|
||||
bind: Some("assist".into()),
|
||||
@ -296,7 +387,7 @@ mod tests {
|
||||
target: "others".into(),
|
||||
},
|
||||
Step {
|
||||
delay_ms: Some(2500),
|
||||
delay_ms: Some(5000),
|
||||
key: None,
|
||||
bind: None,
|
||||
target: "others".into(),
|
||||
@ -414,6 +505,119 @@ mod tests {
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn interact_smart_shortcut_standard_emits_full_sequence() {
|
||||
// bind: "interact" should fire CTM-on, Interact with Target,
|
||||
// sleep walk_delay_ms, CTM-off (ISBoxer Mapped Key analog).
|
||||
let mut e = sample();
|
||||
e.profile.interact.style = crate::profile::InteractStyle::Standard;
|
||||
e.profile.interact.walk_delay_ms = 2500;
|
||||
let map = crate::profile::Map {
|
||||
name: "interact_short".into(),
|
||||
hotkey: Hotkey("Alt+I".into()),
|
||||
hold: false,
|
||||
steps: vec![crate::profile::Step {
|
||||
key: None,
|
||||
bind: Some("interact".into()),
|
||||
delay_ms: None,
|
||||
target: "others".into(),
|
||||
}],
|
||||
release_steps: vec![],
|
||||
};
|
||||
e.profile.maps.push(map);
|
||||
let acts = e.fire("Alt+I", Hold::Tap).unwrap();
|
||||
match &acts[..] {
|
||||
[Action::Send { key: k1, slots: s1, .. },
|
||||
Action::Send { key: k2, slots: s2, .. },
|
||||
Action::Sleep(d),
|
||||
Action::Send { key: k3, slots: s3, .. }] => {
|
||||
assert_eq!(k1, "Shift+F3");
|
||||
assert_eq!(k2, "g");
|
||||
assert_eq!(*d, Duration::from_millis(2500));
|
||||
assert_eq!(k3, "Shift+F4");
|
||||
assert_eq!(s1, &vec![2, 3]);
|
||||
assert_eq!(s2, &vec![2, 3]);
|
||||
assert_eq!(s3, &vec![2, 3]);
|
||||
}
|
||||
other => panic!("expected [ctm_on, interact, sleep, ctm_off], got {other:?}"),
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn interact_smart_shortcut_auto_emits_two_no_tail() {
|
||||
// style = auto: CTM stays on. No sleep, no ctm_off in the press.
|
||||
let mut e = sample();
|
||||
e.profile.interact.style = crate::profile::InteractStyle::Auto;
|
||||
e.profile.interact.walk_delay_ms = 9999;
|
||||
let map = crate::profile::Map {
|
||||
name: "interact_auto".into(),
|
||||
hotkey: Hotkey("Alt+I".into()),
|
||||
hold: false,
|
||||
steps: vec![crate::profile::Step {
|
||||
key: None,
|
||||
bind: Some("interact".into()),
|
||||
delay_ms: None,
|
||||
target: "others".into(),
|
||||
}],
|
||||
release_steps: vec![],
|
||||
};
|
||||
e.profile.maps.push(map);
|
||||
let acts = e.fire("Alt+I", Hold::Tap).unwrap();
|
||||
match &acts[..] {
|
||||
[Action::Send { key: k1, .. }, Action::Send { key: k2, .. }] => {
|
||||
assert_eq!(k1, "Shift+F3");
|
||||
assert_eq!(k2, "g");
|
||||
}
|
||||
other => panic!("expected [ctm_on, interact] only, got {other:?}"),
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn interact_smart_shortcut_hold_emits_press_only() {
|
||||
// style = hold: press fires CTM-on + IWT (Down); ctm_off must be in
|
||||
// release_steps explicitly. The smart shortcut does not emit a tail.
|
||||
let mut e = sample();
|
||||
e.profile.interact.style = crate::profile::InteractStyle::Hold;
|
||||
e.profile.interact.walk_delay_ms = 5000;
|
||||
let map = crate::profile::Map {
|
||||
name: "interact_hold".into(),
|
||||
hotkey: Hotkey("Alt+I".into()),
|
||||
hold: true,
|
||||
steps: vec![crate::profile::Step {
|
||||
key: None,
|
||||
bind: Some("interact".into()),
|
||||
delay_ms: None,
|
||||
target: "others".into(),
|
||||
}],
|
||||
release_steps: vec![crate::profile::Step {
|
||||
key: None,
|
||||
bind: Some("ctm_off".into()),
|
||||
delay_ms: None,
|
||||
target: "others".into(),
|
||||
}],
|
||||
};
|
||||
e.profile.maps.push(map);
|
||||
let press = e.fire("Alt+I", Hold::Down).unwrap();
|
||||
match &press[..] {
|
||||
[Action::Send { key: k1, hold: h1, .. },
|
||||
Action::Send { key: k2, hold: h2, .. }] => {
|
||||
assert_eq!(k1, "Shift+F3");
|
||||
assert_eq!(k2, "g");
|
||||
assert_eq!(*h1, Hold::Down);
|
||||
assert_eq!(*h2, Hold::Down);
|
||||
}
|
||||
other => panic!("press not 2 actions, got {other:?}"),
|
||||
}
|
||||
let release = e.fire("Alt+I", Hold::Up).unwrap();
|
||||
match &release[..] {
|
||||
[Action::Send { key: k, hold: h, .. }] => {
|
||||
assert_eq!(k, "Shift+F4");
|
||||
assert_eq!(*h, Hold::Up);
|
||||
}
|
||||
other => panic!("release not 1 action, got {other:?}"),
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn disabled_mode_intercepts_nothing() {
|
||||
let mut e = sample();
|
||||
|
||||
Loading…
Reference in New Issue
Block a user