Repair mode ends on a right mouse-down in the world and when a targeted spell arms, as in 1.12.1 (#435)

- Casting a targeted spell ends repair mode, as 1.12.1's targeting arm writes its cursor over Repair
- A right mouse-down on the world ends repair mode, as 1.12.1's WorldFrame hook resets the cursor to Point

Fixes #420
Fixes #422

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This commit is contained in:
Sam Sung
2026-09-26 16:23:36 +07:00
committed by GitHub
co-authored by Claude Opus 5.5
parent cc14233a2e
commit 2376fff914
8 changed files with 152 additions and 20 deletions
+13 -1
View File
@@ -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::<crate::run_mode::CaptureMode>))
.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(
+73 -6
View File
@@ -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<CameraControl>,
mut world_right_press: MessageWriter<WorldRightPress>,
) {
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<WorldClick>,
world_right_click: &mut MessageWriter<WorldRightClick>,
world_right_press: &mut MessageWriter<WorldRightPress>,
left_click: &mut Option<PressGesture>,
right_click: &mut Option<PressGesture>,
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::<MouseButton>::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::<CameraControl>();
world.init_resource::<Messages<WorldRightPress>>();
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::<UiScript>().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]
+1 -6
View File
@@ -74,11 +74,7 @@ pub(super) fn control(
mut body: BodyQuery,
window: Single<(&mut Window, &mut CursorOptions), With<PrimaryWindow>>,
// A press starts a look and arms a click test, which the release settles on time and travel.
mut world_clicks: (
MessageWriter<WorldClick>,
MessageWriter<WorldRightClick>,
MessageWriter<WorldRightPress>,
),
mut world_clicks: (MessageWriter<WorldClick>, MessageWriter<WorldRightClick>),
mut click_test: (
Local<Option<camera::PressGesture>>,
Local<Option<camera::PressGesture>>,
@@ -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,
+7 -3
View File
@@ -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,
+17
View File
@@ -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<WorldRightPress>,
payload_held: Res<crate::ui_script::CursorPayloadHeld>,
script: Option<NonSendMut<UiScript>>,
) {
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::*;
+31
View File
@@ -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::<bool>("return InRepairMode() == nil").unwrap());
s.set_spell_targeting(false);
assert!(
!s.repair_mode(),
"ending the targeting restores Point, not Repair"
);
assert!(s.eval::<bool>("return InRepairMode() == nil").unwrap());
}
#[test]
fn clearing_the_merchant_empties_it() {
let mut s = UiScript::new().unwrap();
+8 -2
View File
@@ -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
+2 -2
View File
@@ -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;