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
+1 -19
View File
@@ -198,25 +198,7 @@ impl Loader<'_> {
) {
let color = children_named(region, "Color").next().map(color_of);
if is_texture {
if let Some(atlas) = region.attr("atlas") {
// `atlas="member"` (decision 0950): the Era dialect's texture reference — one
// SetAtlas resolves sheet + UV sub-rect (+ nominal size with `useAtlasSize`)
// through the app-pushed manifest table; `file=` remains the 1.12 MPQ path.
self.call_region(
region_wrapper,
"SetAtlas",
(atlas.to_string(), region.attr_bool("useAtlasSize")),
dbg,
);
if let Some(c) = color {
self.call_region(
region_wrapper,
"SetVertexColor",
(c[0], c[1], c[2], c[3]),
dbg,
);
}
} else if let Some(file) = region.attr("file") {
if let Some(file) = region.attr("file") {
self.call_region(region_wrapper, "SetTexture", file.to_string(), dbg);
if let Some(c) = color {
// A texture with both a file and a <Color> is a tint (vertex color).
-81
View File
@@ -1000,85 +1000,4 @@ mod region_template_tests {
);
assert_eq!(s.eval::<String>("return Label:GetText()").unwrap(), "hello");
}
/// `atlas=` on a Button/CheckButton state texture and on a Slider `<ThumbTexture>` — the Era
/// dialect (0950) extended to the widget texture paths (0957): the slot is created empty and
/// resolved through SetAtlas, so the region answers GetAtlas and (with `useAtlasSize`) wears
/// the member's nominal size. The byte-verified 1.12 loader tables know only file=/<Color>.
#[test]
fn state_textures_and_the_thumb_take_the_atlas_attr() {
let mut s = UiScript::new().unwrap();
s.set_screen_size(800.0, 600.0);
s.set_era_atlases([
(
"checkbox-minimal".to_string(),
crate::script::EraAtlasEntry {
file: "era:textures/4614134.blp".to_string(),
uv: [1.0 / 64.0, 31.0 / 64.0, 1.0 / 64.0, 30.0 / 64.0],
size: [30.0, 29.0],
},
),
(
"checkmark-minimal".to_string(),
crate::script::EraAtlasEntry {
file: "era:textures/4614134.blp".to_string(),
uv: [1.0 / 64.0, 31.0 / 64.0, 32.0 / 64.0, 61.0 / 64.0],
size: [30.0, 29.0],
},
),
(
"minimal_sliderbar_button".to_string(),
crate::script::EraAtlasEntry {
file: "era:textures/4567914.blp".to_string(),
uv: [1.0 / 128.0, 21.0 / 128.0, 20.0 / 128.0, 39.0 / 128.0],
size: [20.0, 19.0],
},
),
]);
let doc = parse(
r#"<Ui>
<CheckButton name="Box">
<Size><AbsDimension x="30" y="29"/></Size>
<Anchors><Anchor point="TOPLEFT" relativePoint="TOPLEFT"/></Anchors>
<NormalTexture atlas="checkbox-minimal"/>
<CheckedTexture atlas="checkmark-minimal"/>
</CheckButton>
<Slider name="Bar" orientation="HORIZONTAL" minValue="0" maxValue="1">
<Size><AbsDimension x="200" y="19"/></Size>
<Anchors><Anchor point="TOPLEFT" relativePoint="TOPLEFT"/></Anchors>
<ThumbTexture atlas="minimal_sliderbar_button" useAtlasSize="true"/>
</Slider>
</Ui>"#,
)
.expect("valid FrameXML");
let report = load(&s, &doc, &no_files);
assert!(report.errors.is_empty(), "errors: {:?}", report.errors);
assert_eq!(
s.eval::<String>("return Box:GetNormalTexture():GetAtlas()")
.unwrap(),
"checkbox-minimal"
);
assert_eq!(
s.eval::<String>("return Box:GetCheckedTexture():GetAtlas()")
.unwrap(),
"checkmark-minimal"
);
assert_eq!(
s.eval::<String>("return Bar:GetThumbTexture():GetAtlas()")
.unwrap(),
"minimal_sliderbar_button"
);
// useAtlasSize rode along — the thumb wears the member's nominal 20×19 (what the
// engine's thumb-travel math reads).
assert_eq!(
s.eval::<f64>("return Bar:GetThumbTexture():GetWidth()")
.unwrap(),
20.0
);
assert_eq!(
s.eval::<f64>("return Bar:GetThumbTexture():GetHeight()")
.unwrap(),
19.0
);
}
}
+8 -29
View File
@@ -85,24 +85,20 @@ impl Loader<'_> {
this.call(wrapper, method, file.to_string(), dbg);
} else if let Some(c) = children_named(t, "Color").next().map(color_of) {
this.call(wrapper, method, (c[0], c[1], c[2], c[3]), dbg);
} else if t.attr("atlas").is_some() {
// `atlas=` (decision 0950, the Era dialect — same seam as a `<Layers>`
// texture's): the byte-verified 1.12 table knows only file=/<Color>, so the
// setter first creates the slot empty and SetAtlas below resolves it.
} else if t.name().is_some() {
// A NAMED state texture with no art of its own — `<NormalTexture
// name="$parentIcon">` on the ref's own MacroFrameButtonTemplate, whose art
// arrives later through `SetTexture`. The setter is what MATERIALIZES the
// slot's region, so without this call the getter below finds nothing and the
// name never publishes: `getglobal("MacroButton1Icon")` would be nil and the
// window's whole Update would die on it. `""` is the live API's own blank form
// (`set_slot_texture`'s empty-string arm), so this creates without painting.
this.call(wrapper, method, String::new(), dbg);
}
// alphaMode / <Size> / <Anchors> / <TexCoords> apply to the region the setter just
// created — fetch it back through the matching getter and use the region methods.
let getter = method.replacen("Set", "Get", 1);
if let Ok(region) = wrapper.call_method::<Table>(getter.as_str(), ()) {
if let Some(atlas) = t.attr("atlas") {
this.call_region(
&region,
"SetAtlas",
(atlas.to_string(), t.attr_bool("useAtlasSize")),
dbg,
);
}
if let Some(mode) = t.attr("alphaMode") {
this.call_region(&region, "SetBlendMode", mode.to_string(), dbg);
}
@@ -338,28 +334,11 @@ impl Loader<'_> {
);
} else if let Some(c) = children_named(tt, "Color").next().map(color_of) {
self.call(wrapper, "SetThumbTexture", (c[0], c[1], c[2], c[3]), dbg);
} else if tt.attr("atlas").is_some() {
// `atlas=` — the Era dialect (0950), same posture as the Button state textures
// above: create the slot empty, resolve via SetAtlas below.
self.call(
wrapper,
"SetThumbTexture",
(String::new(), layer.clone()),
dbg,
);
}
// alphaMode / <Size> / <Anchors> / <TexCoords> apply to the thumb region the setter just
// created — fetch it back through the getter and use the region methods (same shape as a
// Button's state textures).
if let Ok(region) = wrapper.call_method::<Table>("GetThumbTexture", ()) {
if let Some(atlas) = tt.attr("atlas") {
self.call_region(
&region,
"SetAtlas",
(atlas.to_string(), tt.attr_bool("useAtlasSize")),
dbg,
);
}
if let Some(mode) = tt.attr("alphaMode") {
self.call_region(&region, "SetBlendMode", mode.to_string(), dbg);
}