A friendly or resurrection spell with no usable target brings up the targeting cursor, and autoSelfCast boots at 1.12.1's 0 (#445)

A friendly or resurrection spell that binds nothing now enters the unit targeting cursor, as `TryCast` does for any residual word without the enemy bit (`0x6e507b`–`0x6e50ab`). `autoSelfCast` boots at its reference default of `"0"`, and its `Deviates` row is gone.

- **Binding.** The cursor binds a unit through a world click, and through `SpellTargetUnit` from the unit frames (`BindTarget 0x6e5b40`). The selection never moves.
- **Relation checks.** Heals and buffs ask `CanAssist 0x6066f0` instead of reaction alone, so a friendly NPC without the PvP flag is no longer assistable. That is part of #50.
- **Range.** `SpellCanTargetUnit` and the cursor answer per unit through `0x6e6460`'s unit leg, including the spell's min/max range (`6e677c`–`6e6802`). A click on a unit out of range raises "Out of range." or "Target too close" and keeps the cursor armed. The reference's abort there also strands the cursor, and that is not copied.
- **Your own model** joins the world pick only for a word `0x6e61a0` admits (`0x850e`, no `AttributesEx 0x80000`), and never in first person.

Fixes #136

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Co-Authored-By: Sam <samuv.eth@gmail.com>
This commit is contained in:
Juozas Polikevičius
2026-09-27 07:56:21 +07:00
committed by GitHub
co-authored by Claude Opus 5.5 Sam
parent 6982109d86
commit e4cc695f84
22 changed files with 1166 additions and 136 deletions
+23 -4
View File
@@ -1079,12 +1079,31 @@ pub(in crate::script) fn install(lua: &Lua) -> mlua::Result<()> {
})?,
)?;
// SpellTargetUnit(unit): a no-op. A unit-target spell never enters targeting mode here, and for
// the location, item and gameobject modes this client arms, the reference's `BindTarget`
// (`0x6e5b40`) rejects a unit too. Stock `PetFrame_OnClick` calls it, so it must exist.
// SpellTargetUnit(unit) (`0x6e6d90`): an argument that is not a string or a number raises
// `Usage:`; while not targeting the call is a silent no-op, before the token is read; an
// unknown token then raises `Unknown unit name`. The token queues for the host, which ends
// targeting with "Out of range." when it names no unit and otherwise runs `BindTarget`
// (`0x6e5b10` → `0x6e5b40`).
g.set(
"SpellTargetUnit",
lua.create_function(|_, _token: Option<String>| Ok(()))?,
lua.create_function(|lua, token: Value| {
let token = crate::script::binding_abi::string_arg(
lua,
token,
r#"Usage: SpellTargetUnit("unit")"#,
)?;
if !lua
.app_data_ref::<Model>()
.expect("model app_data")
.spell_targeting
{
return Ok(());
}
check_unit_token(&Some(token.clone()))?;
let mut model = lua.app_data_mut::<Model>().expect("model app_data");
model.spell_target_unit.push(token);
Ok(())
})?,
)?;
// ClearTarget(): 1 when it cleared a target, nil when there was none, which `ToggleGameMenu`'s
+5
View File
@@ -411,6 +411,11 @@ impl super::UiScript {
pub fn take_drop_item_on_unit(&mut self) -> Vec<String> {
std::mem::take(&mut self.model_mut().drop_item_on_unit)
}
/// Drain `SpellTargetUnit` calls for the host's pending unit-target spell binder.
pub fn take_spell_target_unit(&mut self) -> Vec<String> {
std::mem::take(&mut self.model_mut().spell_target_unit)
}
}
/// The non-`"player"` token of a two-unit call such as `UnitIsEnemy(a, b)`, which the snapshot
@@ -23,6 +23,56 @@ fn player() -> UnitState {
}
}
/// `0x6e6d90`'s order: the `Usage:` check, the not-targeting no-op, the token check, the queue.
#[test]
fn spell_target_unit_checks_in_the_references_order() {
let mut s = UiScript::new().unwrap();
let usage = |s: &mut UiScript, call: &str| {
let err = s.run(call).expect_err(call).to_string();
assert!(
err.contains(r#"Usage: SpellTargetUnit("unit")"#),
"{call}: {err}"
);
};
// Not targeting: the argument is still type-checked, then nothing, not even the token check.
usage(&mut s, "SpellTargetUnit({})");
usage(&mut s, "SpellTargetUnit()");
s.run(r#"SpellTargetUnit("not-a-unit") SpellTargetUnit("player")"#)
.expect("a bad token is no error while not targeting");
assert!(s.take_spell_target_unit().is_empty(), "and nothing queues");
// Targeting: an unknown token raises, a known one queues, resolved or not.
s.set_spell_targeting(true);
usage(&mut s, "SpellTargetUnit(nil)");
let err = s
.run(r#"SpellTargetUnit("not-a-unit")"#)
.expect_err("an unknown token while targeting")
.to_string();
assert!(err.contains("Unknown unit name: not-a-unit"), "{err}");
s.run(r#"SpellTargetUnit("player") SpellTargetUnit("party1")"#)
.unwrap();
assert_eq!(
s.take_spell_target_unit(),
vec!["player".to_string(), "party1".to_string()]
);
}
#[test]
fn spell_can_target_unit_answers_per_validated_token() {
let mut s = UiScript::new().unwrap();
s.set_spell_targetable_units(["player", "party1"]);
assert!(s
.eval::<bool>(r#"return SpellCanTargetUnit("player") == true"#)
.unwrap());
assert!(s
.eval::<bool>(r#"return SpellCanTargetUnit("PARTY1") == true"#)
.unwrap());
assert!(s
.eval::<bool>(r#"return SpellCanTargetUnit("target") == nil"#)
.unwrap());
}
#[test]
fn unit_reaction_reports_the_scale_value_or_nil() {
let mut s = UiScript::new().unwrap();