diff --git a/crates/benilla-app/src/player.rs b/crates/benilla-app/src/player.rs index 65e4dd92..fe9b8351 100644 --- a/crates/benilla-app/src/player.rs +++ b/crates/benilla-app/src/player.rs @@ -20,7 +20,7 @@ use crate::net::{ClientCommand, Embodied, NetCommands, TeleportMessage, Worldpor use crate::ui_script::InspectMode; use benilla_assets::coords::wow_to_bevy; use benilla_assets::AssetSet; -use benilla_world::interact::{WorldClick, WorldRightClick, WorldRightPress}; +use benilla_world::interact::{WorldClick, WorldRightClick}; use benilla_world::schedule::WorldStage; mod arc; @@ -327,6 +327,18 @@ impl Plugin for PlayerPlugin { .before(follow::steer_follow) .in_set(crate::char_select::InWorldGated), ) + // The world's right mouse-down, off that latch and ahead of `control`, the button's + // binding, as `0x483c40` runs its hook before `ExecuteBinding`; off in capture mode, like + // `control`. + .add_systems( + Update, + camera::send_world_right_press + .in_set(WorldStage::Input) + .after(camera::latch_world_mouse) + .before(control) + .run_if(not(resource_exists::)) + .in_set(crate::char_select::InWorldGated), + ) // `/follow` steers before `control`, so the player's own turn input lands after it and // wins over the follow's steering. .add_systems( diff --git a/crates/benilla-app/src/player/camera.rs b/crates/benilla-app/src/player/camera.rs index 16114d8d..2df4f9e9 100644 --- a/crates/benilla-app/src/player/camera.rs +++ b/crates/benilla-app/src/player/camera.rs @@ -700,6 +700,18 @@ pub(super) fn latch_world_mouse( rig.world_mouse.update(&buttons, world_press); } +/// The right button's down edge in the world, before the press is judged a click or a drag: +/// `CGWorldFrame::OnMouseDown` (`0x483c40`) runs its hook `0x492c20` ahead of the button's binding, +/// so ground targeting's cancel and the repair-mode reset read it before the look session. +pub(super) fn send_world_right_press( + rig: Res, + mut world_right_press: MessageWriter, +) { + if rig.world_mouse.down(LookButton::Right) { + world_right_press.write(WorldRightPress); + } +} + #[derive(Component)] // `pub(crate)` for the scripted camera park's query; `FlyCam::park` is its one lever. pub(crate) struct FlyCam { @@ -747,7 +759,6 @@ pub(super) fn run_look_session( click_consumed: bool, world_click: &mut MessageWriter, world_right_click: &mut MessageWriter, - world_right_press: &mut MessageWriter, left_click: &mut Option, right_click: &mut Option, look_cfg: LookConfig, @@ -757,11 +768,6 @@ pub(super) fn run_look_session( // Seconds on the app clock, for the press predicate's two time gates. now: f32, ) { - // The right button's down edge, before the press is judged a click or a drag: the reference's - // WorldFrame OnMouseDown fires at the press either way. Ground targeting's cancel reads it. - if rig.world_mouse.down(LookButton::Right) { - world_right_press.write(WorldRightPress); - } // A chord is a both-button run, never a select: the reference kills the pending click and arms // none while another primary's binding is held (`0x514ac1`, `0x51481a`). if rig.world_mouse.both() { @@ -1855,6 +1861,67 @@ mod tests { assert!(!rig.world_mouse.held(LookButton::Left)); } + /// A right mouse-down in the world ends repair mode (`0x492c68`), from the latch through the + /// press edge to the reset; the same press on a UI frame, or one that drops a held payload + /// (`0x492b50`), leaves it standing. + #[test] + fn a_right_press_in_the_world_ends_repair_mode_and_one_on_the_ui_does_not() { + use benilla_ui::script::{MerchantState, UiScript}; + use bevy::ecs::system::RunSystemOnce; + use bevy::math::DVec2; + + // One right press with repair mode armed; returns whether repair mode survived it. + let press = |over_ui: bool, payload_held: bool| { + let mut world = World::new(); + let mut buttons = ButtonInput::::default(); + buttons.press(MouseButton::Right); + world.insert_resource(buttons); + world.insert_resource(crate::ui_script::PointerOverUi(over_ui)); + world.insert_resource(crate::ui_script::CursorPayloadHeld(payload_held)); + world.init_resource::(); + world.init_resource::>(); + world.spawn(( + Camera::default(), + FlyCam { + yaw: 0.0, + pitch: 0.0, + speed: 0.0, + }, + )); + let mut window = Window::default(); + window.set_physical_cursor_position(Some(DVec2::new(100.0, 100.0))); + world.spawn((window, PrimaryWindow)); + let mut script = UiScript::new().unwrap(); + script.set_merchant(Some(MerchantState { + can_repair: true, + ..MerchantState::default() + })); + script.run("ShowRepairCursor()").unwrap(); + assert!(script.repair_mode(), "the repair vendor arms repair mode"); + world.insert_non_send_resource(script); + + world.run_system_once(latch_world_mouse).unwrap(); + world.run_system_once(send_world_right_press).unwrap(); + world + .run_system_once(crate::ui_merchant::end_repair_mode_on_right_press) + .unwrap(); + world.non_send_resource::().repair_mode() + }; + + assert!( + !press(false, false), + "a right press in the world ends repair mode" + ); + assert!( + press(true, false), + "a right press on a UI frame never reaches the world's hook" + ); + assert!( + press(false, true), + "a press that drops a held payload is consumed before the hook" + ); + } + /// The classifier (`0x510960`) reads the camera's command word, not the character's velocity, /// and its priority order decides when several states hold. #[test] diff --git a/crates/benilla-app/src/player/controller.rs b/crates/benilla-app/src/player/controller.rs index 33f60779..31433721 100644 --- a/crates/benilla-app/src/player/controller.rs +++ b/crates/benilla-app/src/player/controller.rs @@ -74,11 +74,7 @@ pub(super) fn control( mut body: BodyQuery, window: Single<(&mut Window, &mut CursorOptions), With>, // A press starts a look and arms a click test, which the release settles on time and travel. - mut world_clicks: ( - MessageWriter, - MessageWriter, - MessageWriter, - ), + mut world_clicks: (MessageWriter, MessageWriter), mut click_test: ( Local>, Local>, @@ -244,7 +240,6 @@ pub(super) fn control( click_consumed.0, &mut world_clicks.0, &mut world_clicks.1, - &mut world_clicks.2, left_click, right_click, look_cfg, diff --git a/crates/benilla-app/src/target/mod.rs b/crates/benilla-app/src/target/mod.rs index 279b5427..9bad174d 100644 --- a/crates/benilla-app/src/target/mod.rs +++ b/crates/benilla-app/src/target/mod.rs @@ -282,9 +282,13 @@ impl Plugin for TargetPlugin { hover::update_hover, hover::update_hovered_object, cursor_mode::classify_cursor, - // The right-press cancel (the reference's OnMouseDown hook) before the cursor - // drive, so the press frame already reads the mode cleared. - crate::spell::targeting::cancel_targeting_on_right_press, + // The right press's two legs of the reference's OnMouseDown hook (`0x492c20`), + // the targeting cancel and the repair-mode reset, before the cursor drive, so + // the press frame already reads both modes cleared. + ( + crate::spell::targeting::cancel_targeting_on_right_press, + crate::ui_merchant::end_repair_mode_on_right_press, + ), // Overwrites the classifier's verdict while the targeting cursor is up, as the // reference's dispatcher runs this branch before the object classifier. crate::spell::targeting::drive_targeting_cursor, diff --git a/crates/benilla-app/src/ui_merchant/mod.rs b/crates/benilla-app/src/ui_merchant/mod.rs index 5348a17a..a6d4b281 100644 --- a/crates/benilla-app/src/ui_merchant/mod.rs +++ b/crates/benilla-app/src/ui_merchant/mod.rs @@ -3,6 +3,7 @@ //! [`feed_repair_all_cost`] the repair-all total, and [`drain_merchant`] sends the Lua intents. A bag click sells through [`crate::ui_items`]. use benilla_protocol::messages::{buy_result, sell_result, VendorItem}; +use benilla_world::interact::WorldRightPress; use bevy::prelude::*; use benilla_ui::script::{ItemStatsHead, MerchantItem, MerchantState, ScriptValue, UiScript}; @@ -630,6 +631,22 @@ fn drain_merchant( } } +/// A right mouse-down in the world ends repair mode: the WorldFrame hook `0x492c20` resets the +/// Repair base mode to Point (`0x492c68`) and consumes nothing. A held payload pre-empts the hook +/// (`0x492b50`), and a press over a UI frame never reaches it. +pub(crate) fn end_repair_mode_on_right_press( + mut presses: MessageReader, + payload_held: Res, + script: Option>, +) { + if presses.read().last().is_none() || payload_held.0 { + return; + } + if let Some(mut script) = script { + script.end_repair_mode(); + } +} + #[cfg(test)] mod tests { use super::*; diff --git a/crates/benilla-ui/src/script/merchant.rs b/crates/benilla-ui/src/script/merchant.rs index f0c5f0dd..7593f704 100644 --- a/crates/benilla-ui/src/script/merchant.rs +++ b/crates/benilla-ui/src/script/merchant.rs @@ -128,6 +128,12 @@ impl super::UiScript { pub fn repair_mode(&self) -> bool { self.model_ref().repair_mode } + + /// Leave repair mode, as `HideRepairCursor` does, for the world's right mouse-down, which + /// resets the Repair base mode to Point (`0x492c68`). + pub fn end_repair_mode(&mut self) { + self.model_mut().repair_mode = false; + } } /// 1 or nil, as the client pushes a usable flag (`pushnumber(1.0)` / `pushnil`). @@ -911,6 +917,31 @@ mod tests { assert!(s.take_container_moves().is_empty()); } + /// The targeting arm writes the Cast base mode over Repair (`0x6e50b0`) and its end restores + /// Point (`0x6e49f5`, `0x6e554c`), so arming ends repair mode for good. The idle feed pushes + /// false every frame, which must leave repair mode standing. + #[test] + fn arming_spell_targeting_ends_repair_mode_and_disarming_does_not_restore_it() { + let mut s = UiScript::new().unwrap(); + let mut vendor = stock(); + vendor.can_repair = true; + s.set_merchant(Some(vendor)); + s.run("ShowRepairCursor()").unwrap(); + s.set_spell_targeting(false); + assert!(s.repair_mode(), "the idle feed keeps repair mode"); + + s.set_spell_targeting(true); + assert!(!s.repair_mode(), "the targeting arm ends repair mode"); + assert!(s.eval::("return InRepairMode() == nil").unwrap()); + + s.set_spell_targeting(false); + assert!( + !s.repair_mode(), + "ending the targeting restores Point, not Repair" + ); + assert!(s.eval::("return InRepairMode() == nil").unwrap()); + } + #[test] fn clearing_the_merchant_empties_it() { let mut s = UiScript::new().unwrap(); diff --git a/crates/benilla-ui/src/script/spellbook.rs b/crates/benilla-ui/src/script/spellbook.rs index 0fdf347a..78f2932b 100644 --- a/crates/benilla-ui/src/script/spellbook.rs +++ b/crates/benilla-ui/src/script/spellbook.rs @@ -131,9 +131,15 @@ impl super::UiScript { } /// Whether the spell-targeting cursor is up, for `SpellIsTargeting()` and - /// `SpellStopTargeting()`; pushed each frame before the input pass runs the ESC chain. + /// `SpellStopTargeting()`; pushed each frame before the input pass runs the ESC chain. Arming + /// ends repair mode, as `HideRepairCursor` does: the targeting arm writes the Cast base mode + /// over Repair (`0x6e50b0`), and its end restores Point (`0x6e49f5`, `0x6e554c`), never Repair. pub fn set_spell_targeting(&mut self, targeting: bool) { - self.model_mut().spell_targeting = targeting; + let mut model = self.model_mut(); + model.spell_targeting = targeting; + if targeting { + model.repair_mode = false; + } } /// Drain the `SpellStopTargeting()` trigger, the ESC chain's rung (`UIParent.lua:1490`); the diff --git a/crates/benilla-world/src/interact/mod.rs b/crates/benilla-world/src/interact/mod.rs index dd1b48fa..59af5877 100644 --- a/crates/benilla-world/src/interact/mod.rs +++ b/crates/benilla-world/src/interact/mod.rs @@ -55,8 +55,8 @@ pub struct WorldRightClick; /// The right button's press in the world (off the UI, or any press while a look holds the hidden /// cursor), sent before the click test starts, as the reference's `CGWorldFrame::OnMouseDown` -/// (`0x483c40`), where ground targeting's right-click cancel hangs (`0x492c20`). It consumes -/// nothing, so the turn and the release's context click still run. +/// (`0x483c40`), where ground targeting's right-click cancel and the repair-mode reset hang +/// (`0x492c20`). It consumes nothing, so the turn and the release's context click still run. #[derive(Message, Clone, Copy)] pub struct WorldRightPress;