diff --git a/crates/benilla-ui/src/script/unit/bindings.rs b/crates/benilla-ui/src/script/unit/bindings.rs index 9e4b8ff3..c0d2584f 100644 --- a/crates/benilla-ui/src/script/unit/bindings.rs +++ b/crates/benilla-ui/src/script/unit/bindings.rs @@ -220,6 +220,39 @@ pub(in crate::script) fn install(lua: &Lua) -> mlua::Result<()> { })?, )?; + // UnitCanAssist (`0x516bb0`) delegates to `CanAssist` (`0x6066f0`): the player can help a unit + // whose reaction toward it is friendly, or itself. The snapshot's reaction is the Lua + // `UnitReaction` scale (`0` unknown, 5 Friendly up), and the player token's own reaction stays + // 0, so a both-arguments-`"player"` call answers the self case. + g.set( + "UnitCanAssist", + lua.create_function(|lua, (a, b): (Value, Value)| { + // Both arguments are `lua_isstring`-gated, so either one nil raises. + let a = Some(crate::script::binding_abi::string_arg( + lua, + a, + r#"Usage: UnitCanAssist("unit", "otherUnit")"#, + )?); + let b = Some(crate::script::binding_abi::string_arg( + lua, + b, + r#"Usage: UnitCanAssist("unit", "otherUnit")"#, + )?); + let token = pick_unit_token(&a, &b); + // The player can always assist itself; `"player"` resolves only when both arguments are + // `"player"`, so it is asked outside the snapshot. + let ok = if token + .as_deref() + .is_some_and(|t| t.eq_ignore_ascii_case("player")) + { + true + } else { + with_unit(lua, &token, false, |u| u.exists && u.reaction >= 5)? + }; + Ok(flag(ok)) + })?, + )?; + // GetQuestGreenRange (`0x4e17d0`): the green-to-grey boundary `QuestLogFrame.lua:593` buckets // by, from the table at `0x8076c0`. The reference answers 0 with no player object (`0x4e17f8`); // here a level of 0 reads the table's first entry, 4. diff --git a/crates/benilla-ui/src/script/unit/tests.rs b/crates/benilla-ui/src/script/unit/tests.rs index 70ff364e..40429865 100644 --- a/crates/benilla-ui/src/script/unit/tests.rs +++ b/crates/benilla-ui/src/script/unit/tests.rs @@ -293,6 +293,30 @@ fn corpse_can_attack_and_green_range_bindings() { .unwrap(), 1 ); + // `UnitCanAssist` answers nil for the attackable target above, 1 for a friendly one, and the + // player can always assist itself. + assert!(s + .eval::(r#"return UnitCanAssist("player", "target") == nil"#) + .unwrap()); + s.set_unit( + "target", + Some(UnitState { + exists: true, + reaction: 5, + ..Default::default() + }), + ); + assert_eq!( + s.eval::(r#"return UnitCanAssist("player", "target")"#) + .unwrap(), + 1 + ); + assert_eq!( + s.eval::(r#"return UnitCanAssist("player", "player")"#) + .unwrap(), + 1, + "the player can assist itself" + ); assert!(s .eval::(r#"return UnitIsCorpse("target") == nil"#) .unwrap()); @@ -1435,6 +1459,7 @@ fn a_two_token_predicate_raises_on_either_nil_argument() { "UnitIsFriend", "UnitCanCooperate", "UnitCanAttack", + "UnitCanAssist", "UnitReaction", ] { assert!( @@ -1685,6 +1710,15 @@ fn every_unit_predicate_is_one_or_nil_and_never_a_boolean() { ..Default::default() }, ), + ( + "UnitCanAssist", + r#"UnitCanAssist("player", "target")"#, + UnitState { + exists: true, + reaction: 5, + ..Default::default() + }, + ), ( "UnitIsUnit", r#"UnitIsUnit("target", "target")"#,