pets get their action bar, macros arrive, and hunters draw the bow

- a hunter's or warlock's pet has its action bar now: attack, follow, stay, the pet's own spells with cooldowns and autocast sparkles
- the macro system lands: the macro window, the icon chooser, /cast and /macro, and macros dragged onto the action bar
- hunter shots look right at last: the bow draws and releases on the shot, and the arrow lays a spark trail its whole flight
- the options window re-dresses in 1.12's own art with the classic dialog look, and gets a working search box and clickable steppers

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
Sam
2026-08-05 19:03:13 +07:00
co-authored by Claude Fable 5
parent 994be22f5c
commit 77dc8f8ead
98 changed files with 8922 additions and 1089 deletions
+20 -6
View File
@@ -42,7 +42,7 @@ use bevy::prelude::*;
/// `SLASH_<INDEX><n>` alias-string prefix ([`SlashIndex::key`]). Adding a command is adding a
/// variant + its handler arm; its aliases arrive with the shipped strings.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(super) enum SlashIndex {
pub(crate) enum SlashIndex {
Reply,
Join,
Leave,
@@ -74,6 +74,14 @@ pub(super) enum SlashIndex {
Target,
Assist,
Follow,
/// `/cast <name>` — the reference's `SlashCmdList["CAST"]` is one line, `CastSpellByName(msg)`,
/// and that binding is the engine seam benilla implements over the spell book (decision 0983).
/// The command the whole macro system exists to run.
Cast,
/// `/macro` `/m` — opens the macro window (the ref's `ShowMacroFrame()`).
MacroUi,
/// `/macrohelp` — the reference's own five-line help text.
MacroHelp,
}
impl SlashIndex {
@@ -111,13 +119,16 @@ impl SlashIndex {
Self::Target => "TARGET",
Self::Assist => "ASSIST",
Self::Follow => "FOLLOW",
Self::Cast => "CAST",
Self::MacroUi => "MACRO",
Self::MacroHelp => "MACROHELP",
}
}
/// Every registered index — the registry proper. A reference command NOT in this list resolves
/// nowhere and answers `HELP_TEXT_SIMPLE`, exactly as an unknown command does in the reference
/// (better than a registered handler that silently does nothing).
const ALL: [Self; 31] = [
const ALL: [Self; 34] = [
Self::Reply,
Self::Join,
Self::Leave,
@@ -149,6 +160,9 @@ impl SlashIndex {
Self::Target,
Self::Assist,
Self::Follow,
Self::Cast,
Self::MacroUi,
Self::MacroHelp,
];
}
@@ -156,7 +170,7 @@ impl SlashIndex {
/// (decisions 0099 `/castvis`, 0288 `/chattest`, 0434 `/partytest`, 0600 `/shot`, 0634 `/liquid`,
/// 0637 `/reaction`). Their aliases are literals because there is nothing shipped to read.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(super) enum DevCmd {
pub(crate) enum DevCmd {
CastVis,
ChatTest,
PartyTest,
@@ -189,7 +203,7 @@ impl DevCmd {
/// What a typed `/command` resolved to.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(super) enum Command {
pub(crate) enum Command {
/// A reference `SlashCmdList` command benilla implements.
Slash(SlashIndex),
/// An emote alias — carrying the `EmotesText` id its token resolved to (`/lol` → LAUGH → 45).
@@ -210,14 +224,14 @@ pub(crate) struct SlashCommands {
impl SlashCommands {
/// Resolve a typed command (no leading `/`, any case).
pub(super) fn lookup(&self, cmd: &str) -> Option<Command> {
pub(crate) fn lookup(&self, cmd: &str) -> Option<Command> {
self.by_alias.get(&cmd.to_ascii_lowercase()).copied()
}
/// Build from the reference's alias strings. `get` reads a global string (the UI VM's globals in
/// production, a stub map in tests); `text_id` resolves an `EmotesText` NAME to its id (the
/// emote catalog). Pure over those two, so the whole table is testable without a VM.
pub(super) fn build(
pub(crate) fn build(
get: impl Fn(&str) -> Option<String>,
text_id: impl Fn(&str) -> Option<u32>,
) -> Self {
+19
View File
@@ -562,6 +562,8 @@ pub(super) fn drain_chat_input(
"Duel: /duel [name — bare uses your target], /forfeit (/concede /yield)",
"Social: /who [filter], /friends, /ignore, /trade, /inspect",
"Emotes: /sit /stand /sleep /kneel and every /wave-style emote",
"Spells: /cast <name> [(Rank N)]",
"Macros: /macro (/m) opens the window, /macrohelp explains them",
"Misc: /afk, /dnd, /random [min] [max], /played, /logout, /quit",
"Instruments: /shot, /liquid, /reaction, /castvis, /chattest, /partytest",
] {
@@ -571,6 +573,23 @@ pub(super) fn drain_chat_input(
));
}
}
// `ChatFrame_DisplayMacroHelpText` (ChatFrame.lua): the five shipped lines, read off
// the VM's own GlobalStrings so they are the install's text, never a transcription
// (decision 0983; the 0881 posture — the strings are data, the handler is ours).
ParsedChat::MacroHelp => {
for i in 1..=5 {
let key = format!("MACRO_HELP_TEXT_LINE{i}");
let Ok(line) = script.lua().globals().get::<String>(key.as_str()) else {
continue;
};
if !line.is_empty() {
chat_log.push_event(super::event::ChatEvent::text_only(
super::event::ChatEventKind::System,
line,
));
}
}
}
// `DoEmote` (`0x5ef560`) end to end — wow-re `object-layer/scratch/emote-posture-
// gate.md` §1. The gates in the client's own order, then the two things it DOES: set a
// posture (the `/sit` family) and send the packet.
+24
View File
@@ -172,6 +172,10 @@ pub(in crate::ui_chat) enum ParsedChat {
/// only living assistable ones (the ref's filter mode 2); bare follows the current selection,
/// creature or player. Client-side movement — nothing goes on the wire.
Follow { name: Option<String> },
/// `/macrohelp` — the reference's `ChatFrame_DisplayMacroHelpText`: `MACRO_HELP_TEXT_LINE1..5`
/// straight out of the shipped `GlobalStrings.lua` (decision 0983). Resolved in the drain,
/// which holds the VM, so the text can never go stale against the install.
MacroHelp,
/// A slash line matching neither a chat command nor an `EmotesText` name — dropped.
Unknown,
}
@@ -358,6 +362,26 @@ fn slash_command(index: SlashIndex, args: &str) -> ParsedChat {
},
None => ParsedChat::Unknown,
},
// `/cast <name>` = the ref's `SlashCmdList["CAST"]` verbatim: `if msg ~= "" then
// CastSpellByName(msg) end` (ChatFrame.lua:1120). The binding does the book lookup
// (`benilla_ui::script::spellbook::resolve_spell_by_name`) and queues onto the ONE cast
// path, so a typed `/cast` and a macro's `/cast` line are the same code — decision 0983.
S::Cast => {
if args.is_empty() {
ParsedChat::Unknown
} else {
ParsedChat::Lua {
body: format!("CastSpellByName(\"{}\")", escape_lua_string(args)),
}
}
}
// `/macro` `/m` = the ref's own body: `ShowMacroFrame()` (the `RunMacro(msg)` branch beside
// it is commented out in the shipped 1.12 ChatFrame.lua — there is no `RunMacro` binding in
// the client at all, confirmed against the registered-binding scan).
S::MacroUi => ParsedChat::Lua {
body: "ShowMacroFrame()".into(),
},
S::MacroHelp => ParsedChat::MacroHelp,
// `/script` = the ref's `RunScript(msg)`: the typed text IS the chunk, un-escaped.
S::Script => {
if args.is_empty() {
+35 -4
View File
@@ -650,12 +650,43 @@ fn real_alias_table_resolves_the_shipped_commands() {
}
);
assert_eq!(parse_line("/assist"), ParsedChat::Assist { name: None });
// The macro family (decision 0983): `/cast` (with its `/spell` alias — SLASH_CAST1-4 spell two
// distinct strings across four slots) runs the ref's own one-line body `CastSpellByName(msg)`;
// `/macro`/`/m` open the window; `/macrohelp` prints the shipped five lines.
assert_eq!(
parse_line("/cast Fireball(Rank 1)"),
ParsedChat::Lua {
body: "CastSpellByName(\"Fireball(Rank 1)\")".into()
}
);
assert_eq!(
parse_line("/spell Frostbolt"),
ParsedChat::Lua {
body: "CastSpellByName(\"Frostbolt\")".into()
}
);
assert_eq!(
parse_line("/cast"),
ParsedChat::Unknown,
"a bare /cast is the ref's own no-op (`if msg ~= \"\"`)"
);
for line in ["/macro", "/m"] {
assert_eq!(
parse_line(line),
ParsedChat::Lua {
body: "ShowMacroFrame()".into()
},
"{line}"
);
}
assert_eq!(parse_line("/macrohelp"), ParsedChat::MacroHelp);
// The whole shipped surface, so a table that half-loaded fails loudly: **225 distinct emote
// commands** over the 169 `EmotesText` names (the strings repeat — `EMOTE87_CMD1` and `_CMD2`
// are both "/sit" — and EMOTE27 "UNUSED" has no row, so it contributes none), and **62 distinct
// aliases** across the 31 registered `SlashCmdList` indices (0886 added TARGET's `/target`
// `/tar` and ASSIST's `/assist` `/a` to 0881's 55; 0890 added FOLLOW's `/f` `/follow` `/fol`).
assert_eq!(table.counts(), (62, 225), "(slash, emote) aliases");
// are both "/sit" — and EMOTE27 "UNUSED" has no row, so it contributes none), and **67 distinct
// aliases** across the 34 registered `SlashCmdList` indices (0886 added TARGET's `/target`
// `/tar` and ASSIST's `/assist` `/a` to 0881's 55; 0890 added FOLLOW's `/f` `/follow` `/fol`;
// 0983 added CAST's `/cast` `/spell`, MACRO's `/macro` `/m`, and MACROHELP's `/macrohelp`).
assert_eq!(table.counts(), (67, 225), "(slash, emote) aliases");
}
// ── The send-side posture-eligibility gate (`emote_send_eligible`) — the director-verified rows