fix: ignore a loot right-click while moving, as 1.12.1 does (#790)
- A loot right-click while moving does nothing, as in 1.12.1 (#760) - loot gate: name the emitter's drain and write the landing deviation 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:
co-authored by
Claude Opus 5.5
Sam
parent
9bc7276dd8
commit
4a389add47
@@ -224,6 +224,7 @@ pub(crate) struct Dispatch<'w, 's> {
|
||||
service: ServiceArms<'w>,
|
||||
feedback: Feedback<'w>,
|
||||
auto: crate::player::AutoMove<'w, 's>,
|
||||
player: Res<'w, crate::player::Player>,
|
||||
}
|
||||
|
||||
/// The Loot walk's stop radius: the arm (`0x611130`) takes the square root (`0x6111ab`) of what
|
||||
@@ -246,6 +247,11 @@ impl Dispatch<'_, '_> {
|
||||
self.stores.get(me).ok().map(|(s, _)| s)
|
||||
}
|
||||
|
||||
/// Any `0x20ff` bit in our last streamed movement word: a direction, turn, pitch or fall.
|
||||
fn moving(&self) -> bool {
|
||||
self.player.move_flags() & crate::creature_anim::move_flags::INTEGRATED != 0
|
||||
}
|
||||
|
||||
/// The reference's one mounted predicate, the player's `UNIT_FIELD_MOUNTDISPLAYID`.
|
||||
fn mounted(&self) -> bool {
|
||||
self.self_store()
|
||||
@@ -449,10 +455,13 @@ impl Dispatch<'_, '_> {
|
||||
}
|
||||
}
|
||||
|
||||
/// `0x5df130`: `CMSG_LOOT` at a lootable corpse, walked to first when `walk` and beyond its
|
||||
/// 5 yd (`0x5df1c3`), stopping at [`loot_stop`] of 5. A walk that does not start falls through
|
||||
/// to the send.
|
||||
/// `0x5df130`: `CMSG_LOOT` at a lootable corpse, never while [`Self::moving`] (`0x5df173`),
|
||||
/// walked to first when `walk` and beyond its 5 yd (`0x5df1c3`), stopping at [`loot_stop`] of
|
||||
/// 5. A walk that does not start falls through to the send.
|
||||
fn loot_corpse(&mut self, entity: Entity, guid: u64, walk: bool) {
|
||||
if self.moving() {
|
||||
return;
|
||||
}
|
||||
if walk
|
||||
&& self
|
||||
.dist_sq(entity)
|
||||
@@ -474,10 +483,10 @@ impl Dispatch<'_, '_> {
|
||||
}
|
||||
|
||||
/// `0x5df2a0`: `CMSG_LOOT` at a dead unit, which `can_loot` (`CanLootNow 0x5ec110`) must
|
||||
/// allow, walked to first when `walk` and beyond melee reach. A walk that does not start falls
|
||||
/// through to the send.
|
||||
/// allow, never while [`Self::moving`] (`0x5df2e9`), walked to first when `walk` and beyond
|
||||
/// melee reach. A walk that does not start falls through to the send.
|
||||
fn loot_unit(&mut self, entity: Entity, guid: u64, can_loot: bool, walk: bool) {
|
||||
if !can_loot {
|
||||
if !can_loot || self.moving() {
|
||||
return;
|
||||
}
|
||||
let reach = self.melee_reach(entity);
|
||||
@@ -987,6 +996,14 @@ pub(super) fn act_on_arrival(
|
||||
) {
|
||||
use crate::player::{ApproachVerb, Subject};
|
||||
use benilla_protocol::EntityKind;
|
||||
// Owed while any `0x20ff` bit is set: the reference drains it from the movement emitter
|
||||
// `0x60e0a0` only at a stop, strafe stop, root or unroot ack that leaves none (`0x60e352`), never
|
||||
// at a landing. Deviation: it runs on the first frame with none, so an arrival that sends no stop
|
||||
// (mid-air, rooted, under a held turn key) acts at once or on landing or release, where the
|
||||
// reference waits for the next ground stop and can act long after, wherever the player then is.
|
||||
if dispatch.moving() {
|
||||
return;
|
||||
}
|
||||
let Some((verb, guid)) = dispatch.auto.approach.arrived.take() else {
|
||||
return;
|
||||
};
|
||||
@@ -2551,33 +2568,8 @@ mod tests {
|
||||
/// `0x5df2a0`: beyond melee reach the walk comes first and nothing is looted yet.
|
||||
#[test]
|
||||
fn a_far_body_to_loot_is_walked_to_before_the_loot() {
|
||||
const BODY: u64 = 0xB0D7;
|
||||
const F_DYNAMIC_FLAGS: u16 = 143;
|
||||
let (mut world, _vendor, rx) = walking_world(Vec3::new(14.0, 0.0, 0.0), true);
|
||||
let body = world
|
||||
.spawn((
|
||||
Guid(BODY),
|
||||
store(&[(F_HEALTH, 0), (F_MAXHEALTH, 100), (F_DYNAMIC_FLAGS, 0x1)]),
|
||||
Transform::from_xyz(0.0, 0.0, -12.0),
|
||||
))
|
||||
.id();
|
||||
*world.resource_mut::<PressPick>() = PressPick {
|
||||
hovered: Hovered {
|
||||
target: Some(body),
|
||||
guid: Some(BODY),
|
||||
distance: 12.0,
|
||||
..Hovered::default()
|
||||
},
|
||||
cursor: WorldCursor {
|
||||
kind: cursor_mode::CursorKind::Pickup,
|
||||
unable: false,
|
||||
},
|
||||
..PressPick::default()
|
||||
};
|
||||
world
|
||||
.resource_mut::<Messages<WorldRightClick>>()
|
||||
.write(WorldRightClick);
|
||||
world.run_system_once(act_on_right_click).unwrap();
|
||||
let (mut world, body, corpse, rx) = loot_world(12.0, 0, true);
|
||||
right_click_loot(&mut world, loot_hover(false, body, corpse, 12.0));
|
||||
assert!(world.resource::<crate::player::Approach>().active());
|
||||
assert!(!rx
|
||||
.try_iter()
|
||||
@@ -2589,24 +2581,8 @@ mod tests {
|
||||
/// for a unit.
|
||||
#[test]
|
||||
fn a_loot_walk_stops_at_the_square_root_of_the_melee_reach() {
|
||||
const BODY: u64 = 0xB0D7;
|
||||
const CORPSE: u64 = 0xC0D5;
|
||||
const F_DYNAMIC_FLAGS: u16 = 143;
|
||||
const F_COMBAT_REACH: u16 = 130;
|
||||
let loot_click = |world: &mut World, hovered: Hovered| {
|
||||
*world.resource_mut::<PressPick>() = PressPick {
|
||||
hovered,
|
||||
cursor: WorldCursor {
|
||||
kind: cursor_mode::CursorKind::Pickup,
|
||||
unable: false,
|
||||
},
|
||||
..PressPick::default()
|
||||
};
|
||||
world
|
||||
.resource_mut::<Messages<WorldRightClick>>()
|
||||
.write(WorldRightClick);
|
||||
world.run_system_once(act_on_right_click).unwrap();
|
||||
};
|
||||
// A unit of reach 4 against our default 1.5: 4 + 1.5 + 1.3333 = 6.8333.
|
||||
let (mut world, _vendor, _rx) = walking_world(Vec3::new(14.0, 0.0, 0.0), true);
|
||||
let body = world
|
||||
@@ -2621,7 +2597,7 @@ mod tests {
|
||||
Transform::from_xyz(0.0, 0.0, -12.0),
|
||||
))
|
||||
.id();
|
||||
loot_click(
|
||||
right_click_loot(
|
||||
&mut world,
|
||||
Hovered {
|
||||
target: Some(body),
|
||||
@@ -2647,7 +2623,7 @@ mod tests {
|
||||
Transform::from_xyz(0.0, 0.0, -12.0),
|
||||
))
|
||||
.id();
|
||||
loot_click(
|
||||
right_click_loot(
|
||||
&mut world,
|
||||
Hovered {
|
||||
corpse: Some(corpse),
|
||||
@@ -3002,4 +2978,170 @@ mod tests {
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
const BODY: u64 = 0xB0D7;
|
||||
const CORPSE: u64 = 0xC0D5;
|
||||
|
||||
/// A dead lootable unit and a lootable corpse `at` yd off and the vendor in reach, with our last
|
||||
/// streamed movement word `flags` and Click to Move set as `walk`.
|
||||
fn loot_world(
|
||||
at: f32,
|
||||
flags: u32,
|
||||
walk: bool,
|
||||
) -> (
|
||||
World,
|
||||
Entity,
|
||||
Entity,
|
||||
crossbeam_channel::Receiver<ClientCommand>,
|
||||
) {
|
||||
const F_DYNAMIC_FLAGS: u16 = 143;
|
||||
let (mut world, _vendor, rx) = walking_world(Vec3::new(2.5, 0.0, 0.0), walk);
|
||||
world.insert_resource(crate::player::Player::with_move_flags(flags));
|
||||
let body = world
|
||||
.spawn((
|
||||
Guid(BODY),
|
||||
store(&[(F_HEALTH, 0), (F_MAXHEALTH, 100), (F_DYNAMIC_FLAGS, 0x1)]),
|
||||
Transform::from_xyz(0.0, 0.0, -at),
|
||||
crate::net::NetEntity {
|
||||
kind: benilla_protocol::EntityKind::Unit,
|
||||
display_id: None,
|
||||
scale: 1.0,
|
||||
},
|
||||
))
|
||||
.id();
|
||||
world
|
||||
.resource_mut::<crate::net::GuidIndex>()
|
||||
.0
|
||||
.insert(BODY, body);
|
||||
let corpse = world
|
||||
.spawn((
|
||||
Guid(CORPSE),
|
||||
store(&[(benilla_protocol::field::FIELD_CORPSE_DYNAMIC_FLAGS, 0x1)]),
|
||||
Transform::from_xyz(0.0, 0.0, at),
|
||||
))
|
||||
.id();
|
||||
(world, body, corpse, rx)
|
||||
}
|
||||
|
||||
fn right_click_loot(world: &mut World, hovered: Hovered) {
|
||||
*world.resource_mut::<PressPick>() = PressPick {
|
||||
hovered,
|
||||
cursor: WorldCursor {
|
||||
kind: cursor_mode::CursorKind::Pickup,
|
||||
unable: false,
|
||||
},
|
||||
..PressPick::default()
|
||||
};
|
||||
world
|
||||
.resource_mut::<Messages<WorldRightClick>>()
|
||||
.write(WorldRightClick);
|
||||
world.run_system_once(act_on_right_click).unwrap();
|
||||
}
|
||||
|
||||
/// The press over the corpse when `on_corpse`, else over the dead unit.
|
||||
fn loot_hover(on_corpse: bool, body: Entity, corpse: Entity, at: f32) -> Hovered {
|
||||
let (unit, object) = if on_corpse {
|
||||
(None, Some(corpse))
|
||||
} else {
|
||||
(Some(body), None)
|
||||
};
|
||||
Hovered {
|
||||
target: unit,
|
||||
guid: unit.map(|_| BODY),
|
||||
corpse: object,
|
||||
corpse_guid: object.map(|_| CORPSE),
|
||||
distance: at,
|
||||
..Hovered::default()
|
||||
}
|
||||
}
|
||||
|
||||
/// `0x5df2e9` and `0x5df173`: both loot senders return silently on any `0x20ff` bit, a
|
||||
/// direction, turn, pitch or fall; swimming and walk mode are outside the mask.
|
||||
#[test]
|
||||
fn a_loot_click_while_moving_sends_nothing() {
|
||||
use crate::creature_anim::move_flags as f;
|
||||
for (flags, loots) in [
|
||||
(0, true),
|
||||
(f::FORWARD, false),
|
||||
(f::BACKWARD, false),
|
||||
(f::STRAFE_LEFT, false),
|
||||
(f::TURN_RIGHT, false),
|
||||
(f::FALLING, false),
|
||||
(f::FORWARD | f::FALLING, false),
|
||||
(f::SWIMMING, true),
|
||||
(f::WALK_MODE, true),
|
||||
] {
|
||||
for on_corpse in [false, true] {
|
||||
let (mut world, body, corpse, rx) = loot_world(2.0, flags, false);
|
||||
right_click_loot(&mut world, loot_hover(on_corpse, body, corpse, 2.0));
|
||||
let looted = rx
|
||||
.try_iter()
|
||||
.any(|c| matches!(c, ClientCommand::Loot { .. }));
|
||||
let latched = world.resource::<crate::ui_loot::LootLatch>().0.is_some();
|
||||
assert_eq!(
|
||||
(looted, latched),
|
||||
(loots, loots),
|
||||
"flags {flags:#x}, corpse {on_corpse}"
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// The movement test comes before the walk (`0x5df2f6`, `0x5df180`): a far body or corpse
|
||||
/// clicked on the run starts no Click to Move walk.
|
||||
#[test]
|
||||
fn a_far_loot_click_while_moving_starts_no_walk() {
|
||||
use crate::creature_anim::move_flags as f;
|
||||
for (flags, walks) in [(0, true), (f::FORWARD, false), (f::FALLING, false)] {
|
||||
for on_corpse in [false, true] {
|
||||
let (mut world, body, corpse, rx) = loot_world(12.0, flags, true);
|
||||
right_click_loot(&mut world, loot_hover(on_corpse, body, corpse, 12.0));
|
||||
assert_eq!(
|
||||
world.resource::<crate::player::Approach>().active(),
|
||||
walks,
|
||||
"flags {flags:#x}, corpse {on_corpse}"
|
||||
);
|
||||
assert!(!rx
|
||||
.try_iter()
|
||||
.any(|c| matches!(c, ClientCommand::Loot { .. })));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// `0x60e352`: the owed verb (`0x60fa20`) runs only once no `0x20ff` bit is left, and the cell
|
||||
/// stays armed until then.
|
||||
#[test]
|
||||
fn a_verb_owed_on_arrival_waits_for_a_word_with_no_move_bit() {
|
||||
use crate::creature_anim::move_flags as f;
|
||||
use crate::player::ApproachVerb;
|
||||
for (verb, guid) in [(ApproachVerb::Loot, BODY), (ApproachVerb::Talk, VENDOR)] {
|
||||
let (mut world, _body, _corpse, rx) = loot_world(2.0, f::FALLING, true);
|
||||
world.resource_mut::<crate::player::Approach>().arrived = Some((verb, guid));
|
||||
world.run_system_once(act_on_arrival).unwrap();
|
||||
assert_eq!(rx.try_iter().count(), 0, "{verb:?} mid-air");
|
||||
assert_eq!(
|
||||
world.resource::<crate::player::Approach>().arrived,
|
||||
Some((verb, guid)),
|
||||
"{verb:?} stays owed"
|
||||
);
|
||||
world.insert_resource(crate::player::Player::with_move_flags(0));
|
||||
world.run_system_once(act_on_arrival).unwrap();
|
||||
let sent: Vec<ClientCommand> = rx.try_iter().collect();
|
||||
assert!(
|
||||
match verb {
|
||||
ApproachVerb::Loot =>
|
||||
matches!(sent.as_slice(), [ClientCommand::Loot { guid: BODY }]),
|
||||
_ => matches!(
|
||||
sent.as_slice(),
|
||||
[ClientCommand::ListInventory { guid: VENDOR }]
|
||||
),
|
||||
},
|
||||
"{verb:?} landed: {sent:?}"
|
||||
);
|
||||
assert!(world
|
||||
.resource::<crate::player::Approach>()
|
||||
.arrived
|
||||
.is_none());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user