1use ghast_data::DigMaterial;
17use ghast_protocol::BlockPos;
18
19use crate::common::{DigStatus, Face, Outbound, Vec3};
20
21pub const REACH: f64 = 4.5;
23
24#[derive(Debug, Clone, Copy, PartialEq)]
26#[non_exhaustive]
27pub struct BlockQuery {
28 pub air: bool,
30 pub water: bool,
32 pub block: u16,
34 pub hardness: f32,
36 pub dig_material: Option<DigMaterial>,
38 pub harvest_tools: &'static [u16],
40 pub selection: Option<[f64; 6]>,
42 pub stairs: Option<[Option<[f64; 6]>; 8]>,
45 pub activates: bool,
47}
48
49#[derive(Debug, Clone, Copy, Default)]
51#[non_exhaustive]
52pub struct HeldItem {
53 pub id: u16,
55 pub count: u8,
57 pub efficiency: u8,
59 pub places_block: bool,
61 pub usable: bool,
63 pub food: bool,
65 pub always_edible: bool,
67 pub tool_speed: Option<fn(DigMaterial, u16) -> f32>,
69}
70
71impl BlockQuery {
72 #[must_use]
75 pub const fn new(block: u16) -> Self {
76 BlockQuery {
77 air: false,
78 water: false,
79 block,
80 hardness: 0.0,
81 dig_material: None,
82 harvest_tools: &[],
83 selection: None,
84 stairs: None,
85 activates: false,
86 }
87 }
88
89 pub const AIR: BlockQuery = BlockQuery::new(0).with_air(true);
91
92 #[must_use]
94 pub const fn with_air(mut self, on: bool) -> Self {
95 self.air = on;
96 self
97 }
98
99 #[must_use]
101 pub const fn with_water(mut self, on: bool) -> Self {
102 self.water = on;
103 self
104 }
105
106 #[must_use]
108 pub const fn with_hardness(mut self, hardness: f32) -> Self {
109 self.hardness = hardness;
110 self
111 }
112
113 #[must_use]
115 pub const fn with_dig_material(mut self, m: Option<DigMaterial>) -> Self {
116 self.dig_material = m;
117 self
118 }
119
120 #[must_use]
122 pub const fn with_harvest_tools(mut self, tools: &'static [u16]) -> Self {
123 self.harvest_tools = tools;
124 self
125 }
126
127 #[must_use]
129 pub const fn with_selection(mut self, b: Option<[f64; 6]>) -> Self {
130 self.selection = b;
131 self
132 }
133
134 #[must_use]
136 pub const fn with_stairs(mut self, passes: Option<[Option<[f64; 6]>; 8]>) -> Self {
137 self.stairs = passes;
138 self
139 }
140
141 #[must_use]
143 pub const fn with_activates(mut self, on: bool) -> Self {
144 self.activates = on;
145 self
146 }
147}
148
149impl HeldItem {
150 #[must_use]
152 pub const fn new(id: u16) -> Self {
153 HeldItem {
154 id,
155 count: 1,
156 efficiency: 0,
157 places_block: false,
158 usable: false,
159 food: false,
160 always_edible: false,
161 tool_speed: None,
162 }
163 }
164
165 #[must_use]
167 pub const fn with_count(mut self, count: u8) -> Self {
168 self.count = count;
169 self
170 }
171
172 #[must_use]
174 pub const fn with_efficiency(mut self, level: u8) -> Self {
175 self.efficiency = level;
176 self
177 }
178
179 #[must_use]
181 pub const fn with_places_block(mut self, on: bool) -> Self {
182 self.places_block = on;
183 self
184 }
185
186 #[must_use]
188 pub const fn with_usable(mut self, on: bool) -> Self {
189 self.usable = on;
190 self
191 }
192
193 #[must_use]
195 pub const fn with_food(mut self, food: bool, always_edible: bool) -> Self {
196 self.food = food;
197 self.always_edible = always_edible;
198 self
199 }
200
201 #[must_use]
203 pub const fn with_tool_speed(mut self, f: Option<fn(DigMaterial, u16) -> f32>) -> Self {
204 self.tool_speed = f;
205 self
206 }
207}
208
209impl DigFactors {
210 #[must_use]
212 pub const fn with_on_ground(mut self, on: bool) -> Self {
213 self.on_ground = on;
214 self
215 }
216
217 #[must_use]
219 pub const fn with_in_water(mut self, on: bool) -> Self {
220 self.in_water = on;
221 self
222 }
223
224 #[must_use]
226 pub const fn with_aqua_affinity(mut self, on: bool) -> Self {
227 self.aqua_affinity = on;
228 self
229 }
230
231 #[must_use]
233 pub const fn with_haste(mut self, level: u8) -> Self {
234 self.haste = level;
235 self
236 }
237
238 #[must_use]
240 pub const fn with_fatigue(mut self, level: u8) -> Self {
241 self.fatigue = level;
242 self
243 }
244}
245
246#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
248#[non_exhaustive]
249pub struct DigFactors {
250 pub on_ground: bool,
252 pub in_water: bool,
254 pub aqua_affinity: bool,
256 pub haste: u8,
258 pub fatigue: u8,
260}
261
262#[must_use]
264pub fn relative_hardness(b: &BlockQuery, held: Option<&HeldItem>, f: DigFactors) -> f32 {
265 let h = b.hardness;
266 if h < 0.0 {
267 return 0.0;
268 }
269 let can_harvest =
270 b.harvest_tools.is_empty() || held.is_some_and(|i| b.harvest_tools.contains(&i.id));
271 let mut speed = match (held, b.dig_material) {
273 (Some(i), Some(m)) => i.tool_speed.map_or(1.0, |t| t(m, i.id)),
274 _ => 1.0,
275 };
276 if speed > 1.0
277 && let Some(i) = held
278 && i.efficiency > 0
279 {
280 let e = f32::from(i.efficiency);
281 speed += e * e + 1.0;
282 }
283 if f.haste > 0 {
284 speed *= 1.0 + f32::from(f.haste) * 0.2;
285 }
286 if f.fatigue > 0 {
287 speed *= match f.fatigue {
288 1 => 0.3,
289 2 => 0.09,
290 3 => 0.0027,
291 _ => 8.1e-4,
292 };
293 }
294 if f.in_water && !f.aqua_affinity {
295 speed /= 5.0;
296 }
297 if !f.on_ground {
298 speed /= 5.0;
299 }
300 if can_harvest {
301 speed / h / 30.0
302 } else {
303 speed / h / 100.0
304 }
305}
306
307#[must_use]
309pub fn look_vector(yaw: f32, pitch: f32) -> Vec3 {
310 use ghast_physics::math::{cos, sin};
311 const RAD: f32 = 0.017_453_292;
312 let f = cos(-yaw * RAD - std::f32::consts::PI);
313 let f1 = sin(-yaw * RAD - std::f32::consts::PI);
314 let f2 = -cos(-pitch * RAD);
315 let f3 = sin(-pitch * RAD);
316 Vec3::new(f64::from(f1 * f2), f64::from(f3), f64::from(f * f2))
317}
318
319#[derive(Debug, Clone, Copy, PartialEq)]
321#[non_exhaustive]
322pub struct BlockHit {
323 pub pos: BlockPos,
325 pub face: Face,
327 pub hit: Vec3,
329 pub distance: f64,
331}
332
333fn floor(v: f64) -> i32 {
335 let i = v as i32;
336 if v < f64::from(i) { i - 1 } else { i }
337}
338
339fn intermediate(a: Vec3, b: Vec3, axis: usize, v: f64) -> Option<Vec3> {
342 let d = [b.x - a.x, b.y - a.y, b.z - a.z];
343 let o = [a.x, a.y, a.z];
344 if d[axis] * d[axis] < 1.0e-7 {
345 return None;
346 }
347 let t = (v - o[axis]) / d[axis];
348 (0.0..=1.0)
349 .contains(&t)
350 .then(|| Vec3::new(a.x + d[0] * t, a.y + d[1] * t, a.z + d[2] * t))
351}
352
353fn sq_dist(a: Vec3, b: Vec3) -> f64 {
354 let (x, y, z) = (b.x - a.x, b.y - a.y, b.z - a.z);
355 x * x + y * y + z * z
356}
357
358fn block_ray(pos: BlockPos, b: &[f64; 6], start: Vec3, end: Vec3) -> Option<(Vec3, Face)> {
362 let off = Vec3::new(f64::from(pos.x), f64::from(pos.y), f64::from(pos.z));
363 let s = Vec3::new(start.x - off.x, start.y - off.y, start.z - off.z);
364 let e = Vec3::new(end.x - off.x, end.y - off.y, end.z - off.z);
365 let in_yz = |v: &Vec3| v.y >= b[1] && v.y <= b[4] && v.z >= b[2] && v.z <= b[5];
366 let in_xz = |v: &Vec3| v.x >= b[0] && v.x <= b[3] && v.z >= b[2] && v.z <= b[5];
367 let in_xy = |v: &Vec3| v.x >= b[0] && v.x <= b[3] && v.y >= b[1] && v.y <= b[4];
368 let candidates = [
369 (intermediate(s, e, 0, b[0]).filter(in_yz), Face::West),
370 (intermediate(s, e, 0, b[3]).filter(in_yz), Face::East),
371 (intermediate(s, e, 1, b[1]).filter(in_xz), Face::Down),
372 (intermediate(s, e, 1, b[4]).filter(in_xz), Face::Up),
373 (intermediate(s, e, 2, b[2]).filter(in_xy), Face::North),
374 (intermediate(s, e, 2, b[5]).filter(in_xy), Face::South),
375 ];
376 let mut best: Option<(Vec3, Face)> = None;
377 for (v, face) in candidates {
378 if let Some(v) = v
379 && best.is_none_or(|(bv, _)| sq_dist(s, v) < sq_dist(s, bv))
380 {
381 best = Some((v, face));
382 }
383 }
384 best.map(|(v, f)| (Vec3::new(v.x + off.x, v.y + off.y, v.z + off.z), f))
385}
386
387#[derive(Debug, Clone, Copy, PartialEq)]
389#[non_exhaustive]
390pub enum RayResult {
391 Block(BlockHit),
393 Miss {
396 pos: BlockPos,
398 face: Face,
400 hit: Vec3,
402 },
403 None,
405}
406
407pub fn ray_trace(
412 blocks: &dyn Fn(i32, i32, i32) -> Option<BlockQuery>,
413 start: Vec3,
414 end: Vec3,
415 return_last: bool,
416) -> RayResult {
417 let nan = |v: Vec3| v.x.is_nan() || v.y.is_nan() || v.z.is_nan();
418 if nan(start) || nan(end) {
419 return RayResult::None;
420 }
421 let dist = |hit: Vec3, eyes: Vec3| sq_dist(eyes, hit).sqrt();
422 let eyes = start;
423 let (ex, ey, ez) = (floor(end.x), floor(end.y), floor(end.z));
424 let (mut x, mut y, mut z) = (floor(start.x), floor(start.y), floor(start.z));
425 let test = |p: BlockPos, s: Vec3| -> Option<BlockHit> {
426 let b = blocks(p.x, p.y, p.z)?;
427 let found = if let Some(passes) = &b.stairs {
428 let mut best: Option<(Vec3, Face)> = None;
431 let mut best_d = 0.0;
432 for bounds in passes.iter().flatten() {
433 if let Some((hit, face)) = block_ray(p, bounds, s, end) {
434 let d = sq_dist(hit, end);
435 if d > best_d {
436 best = Some((hit, face));
437 best_d = d;
438 }
439 }
440 }
441 best
442 } else {
443 block_ray(p, &b.selection?, s, end)
444 };
445 found.map(|(hit, face)| BlockHit {
446 pos: p,
447 face,
448 hit,
449 distance: dist(hit, eyes),
450 })
451 };
452 let first = BlockPos::new(x, y, z);
453 if let Some(h) = test(first, start) {
454 return RayResult::Block(h);
455 }
456 let mut last = RayResult::None;
457 let mut cur = start;
458 let mut steps = 200;
459 while steps >= 0 {
460 steps -= 1;
461 if nan(cur) {
462 return RayResult::None;
463 }
464 if x == ex && y == ey && z == ez {
465 break;
466 }
467 let plane = |e: i32, c: i32| {
468 if e > c {
469 Some(f64::from(c) + 1.0)
470 } else if e < c {
471 Some(f64::from(c))
472 } else {
473 None
474 }
475 };
476 let (px, py, pz) = (plane(ex, x), plane(ey, y), plane(ez, z));
477 let (dx, dy, dz) = (end.x - cur.x, end.y - cur.y, end.z - cur.z);
478 let frac = |p: Option<f64>, o: f64, d: f64| {
479 let t = p.map_or(999.0, |p| (p - o) / d);
480 if t == 0.0 { -1.0e-4 } else { t }
482 };
483 let (tx, ty, tz) = (
484 frac(px, cur.x, dx),
485 frac(py, cur.y, dy),
486 frac(pz, cur.z, dz),
487 );
488 let face;
489 if tx < ty && tx < tz {
490 face = if ex > x { Face::West } else { Face::East };
491 cur = Vec3::new(px.unwrap_or(999.0), cur.y + dy * tx, cur.z + dz * tx);
492 } else if ty < tz {
493 face = if ey > y { Face::Down } else { Face::Up };
494 cur = Vec3::new(cur.x + dx * ty, py.unwrap_or(999.0), cur.z + dz * ty);
495 } else {
496 face = if ez > z { Face::North } else { Face::South };
497 cur = Vec3::new(cur.x + dx * tz, cur.y + dy * tz, pz.unwrap_or(999.0));
498 }
499 x = floor(cur.x) - i32::from(face == Face::East);
500 y = floor(cur.y) - i32::from(face == Face::Up);
501 z = floor(cur.z) - i32::from(face == Face::South);
502 let p = BlockPos::new(x, y, z);
503 match blocks(x, y, z).filter(|b| b.selection.is_some() || b.stairs.is_some()) {
504 Some(_) => {
505 if let Some(h) = test(p, cur) {
506 return RayResult::Block(h);
507 }
508 }
509 None => {
510 last = RayResult::Miss {
511 pos: p,
512 face,
513 hit: cur,
514 }
515 }
516 }
517 }
518 if return_last { last } else { RayResult::None }
519}
520
521#[must_use]
523pub fn pick(
524 blocks: &dyn Fn(i32, i32, i32) -> Option<BlockQuery>,
525 eyes: Vec3,
526 look: Vec3,
527 reach: f64,
528) -> Option<BlockHit> {
529 let end = Vec3::new(
530 eyes.x + look.x * reach,
531 eyes.y + look.y * reach,
532 eyes.z + look.z * reach,
533 );
534 match ray_trace(blocks, eyes, end, true) {
535 RayResult::Block(h) => Some(h),
536 _ => None,
537 }
538}
539
540#[derive(Debug, Clone, Copy, PartialEq)]
542#[non_exhaustive]
543pub struct EntityHit {
544 pub id: i32,
546 pub hit: Vec3,
548 pub entity_pos: Vec3,
550 pub distance: f64,
552}
553
554fn box_intercept(b: &[f64; 6], a: Vec3, e: Vec3) -> Option<Vec3> {
557 let in_yz = |v: &Vec3| v.y >= b[1] && v.y <= b[4] && v.z >= b[2] && v.z <= b[5];
558 let in_xz = |v: &Vec3| v.x >= b[0] && v.x <= b[3] && v.z >= b[2] && v.z <= b[5];
559 let in_xy = |v: &Vec3| v.x >= b[0] && v.x <= b[3] && v.y >= b[1] && v.y <= b[4];
560 let cands = [
561 intermediate(a, e, 0, b[0]).filter(in_yz),
562 intermediate(a, e, 0, b[3]).filter(in_yz),
563 intermediate(a, e, 1, b[1]).filter(in_xz),
564 intermediate(a, e, 1, b[4]).filter(in_xz),
565 intermediate(a, e, 2, b[2]).filter(in_xy),
566 intermediate(a, e, 2, b[5]).filter(in_xy),
567 ];
568 let mut best: Option<Vec3> = None;
569 for v in cands.into_iter().flatten() {
570 if best.is_none_or(|bv| sq_dist(a, v) < sq_dist(a, bv)) {
571 best = Some(v);
572 }
573 }
574 best
575}
576
577#[must_use]
580pub(crate) fn ray_hits_cube(eyes: Vec3, look: Vec3, pos: BlockPos, reach: f64) -> bool {
581 let (x, y, z) = (f64::from(pos.x), f64::from(pos.y), f64::from(pos.z));
582 let b = [x, y, z, x + 1.0, y + 1.0, z + 1.0];
583 let inside = eyes.x >= b[0]
584 && eyes.x <= b[3]
585 && eyes.y >= b[1]
586 && eyes.y <= b[4]
587 && eyes.z >= b[2]
588 && eyes.z <= b[5];
589 let end = Vec3::new(
590 eyes.x + look.x * reach,
591 eyes.y + look.y * reach,
592 eyes.z + look.z * reach,
593 );
594 inside || box_intercept(&b, eyes, end).is_some()
595}
596
597pub type PickCandidate = (i32, [f64; 6], Vec3);
599
600#[must_use]
604pub fn pick_entity(
605 candidates: impl Iterator<Item = PickCandidate>,
606 eyes: Vec3,
607 look: Vec3,
608 block_distance: Option<f64>,
609 border: f64,
610 riding: Option<i32>,
611) -> Option<EntityHit> {
612 let d0 = REACH;
613 let d1 = block_distance.unwrap_or(d0);
614 let end = Vec3::new(
615 eyes.x + look.x * d0,
616 eyes.y + look.y * d0,
617 eyes.z + look.z * d0,
618 );
619 let mut best: Option<EntityHit> = None;
620 let mut d2 = d1;
621 for (id, b, pos) in candidates {
622 let g = [
623 b[0] - border,
624 b[1] - border,
625 b[2] - border,
626 b[3] + border,
627 b[4] + border,
628 b[5] + border,
629 ];
630 let hit = box_intercept(&g, eyes, end);
631 let inside = eyes.x > g[0]
632 && eyes.x < g[3]
633 && eyes.y > g[1]
634 && eyes.y < g[4]
635 && eyes.z > g[2]
636 && eyes.z < g[5];
637 if inside {
638 if d2 >= 0.0 {
639 best = Some(EntityHit {
640 id,
641 hit: hit.unwrap_or(eyes),
642 entity_pos: pos,
643 distance: 0.0,
644 });
645 d2 = 0.0;
646 }
647 } else if let Some(h) = hit {
648 let d3 = sq_dist(eyes, h).sqrt();
649 if d3 < d2 || d2 == 0.0 {
650 if riding == Some(id) {
651 if d2 == 0.0 {
652 best = Some(EntityHit {
653 id,
654 hit: h,
655 entity_pos: pos,
656 distance: d3,
657 });
658 }
659 } else {
660 best = Some(EntityHit {
661 id,
662 hit: h,
663 entity_pos: pos,
664 distance: d3,
665 });
666 d2 = d3;
667 }
668 }
669 }
670 }
671 let e = best?;
672 if sq_dist(eyes, e.hit).sqrt() > 3.0 {
674 return None;
675 }
676 (d2 < d1 || block_distance.is_none()).then_some(e)
677}
678
679#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
681pub struct Buttons {
682 pub left: bool,
684 pub right: bool,
686}
687
688pub(crate) struct HandsIo<'a> {
690 pub blocks: &'a dyn Fn(i32, i32, i32) -> Option<BlockQuery>,
692 pub target: Option<BlockHit>,
694 pub held: Option<HeldItem>,
696 pub held_stack: Option<ghast_inventory::Stack>,
698 pub food: i32,
700 pub modern: bool,
702 pub off_held: Option<HeldItem>,
704 pub off_stack: Option<ghast_inventory::Stack>,
706 pub factors: DigFactors,
708 pub sneaking: bool,
710 pub entity: Option<EntityHit>,
712 pub out: &'a mut Vec<Outbound>,
714 pub broken: &'a mut Vec<BlockPos>,
716 pub placed: &'a mut bool,
718}
719
720impl HandsIo<'_> {
721 fn solid(&self, pos: BlockPos) -> Option<BlockQuery> {
725 if self.broken.contains(&pos) {
726 return None;
727 }
728 (self.blocks)(pos.x, pos.y, pos.z).filter(|b| !b.air)
729 }
730}
731
732#[derive(Debug, Clone, Default)]
734pub struct Hands {
735 hitting: bool,
736 current: Option<BlockPos>,
737 damage: f32,
738 hit_item: Option<u16>,
739 hit_delay: u32,
740 left_click_counter: u32,
741 right_click_delay: u32,
742 using: bool,
743 left_was: bool,
744 right_was: bool,
745 pub broken: u64,
747 pub ticks_since_swing: u32,
749}
750
751impl Hands {
752 pub fn reset(&mut self) {
754 let broken = self.broken;
755 *self = Hands::default();
756 self.broken = broken;
757 }
758
759 #[must_use]
761 pub fn mining(&self) -> Option<(BlockPos, f32)> {
762 match (self.hitting, self.current) {
763 (true, Some(p)) => Some((p, self.damage.min(1.0))),
764 _ => None,
765 }
766 }
767
768 #[must_use]
770 pub fn using_item(&self) -> bool {
771 self.using
772 }
773
774 pub fn stop_using(&mut self) {
776 self.using = false;
777 }
778
779 #[must_use]
783 pub fn attack_strength(&self, attack_speed: f64) -> f32 {
784 let period = (1.0 / attack_speed * 20.0) as f32;
785 ((self.ticks_since_swing as f32 + 0.5) / period).clamp(0.0, 1.0)
786 }
787
788 pub(crate) fn attack(&mut self, id: i32, io: &mut HandsIo<'_>) {
790 io.out.push(Outbound::UseEntity {
791 target: id,
792 action: crate::common::EntityUse::Attack,
793 off_hand: false,
794 sneaking: io.sneaking,
795 });
796 self.ticks_since_swing = 0;
797 }
798
799 fn interact(&mut self, e: EntityHit, off_hand: bool, io: &mut HandsIo<'_>) {
802 let rel = Vec3::new(
803 e.hit.x - e.entity_pos.x,
804 e.hit.y - e.entity_pos.y,
805 e.hit.z - e.entity_pos.z,
806 );
807 io.out.push(Outbound::UseEntity {
808 target: e.id,
809 action: crate::common::EntityUse::InteractAt(rel),
810 off_hand,
811 sneaking: io.sneaking,
812 });
813 io.out.push(Outbound::UseEntity {
814 target: e.id,
815 action: crate::common::EntityUse::Interact,
816 off_hand,
817 sneaking: io.sneaking,
818 });
819 }
820
821 pub(crate) fn tick(&mut self, left: bool, right: bool, io: &mut HandsIo<'_>) {
822 self.ticks_since_swing = self.ticks_since_swing.saturating_add(1);
823 if self.left_click_counter > 0 {
824 self.left_click_counter -= 1;
825 }
826 if self.right_click_delay > 0 {
827 self.right_click_delay -= 1;
828 }
829 let t = io.target;
830 if self.using {
831 if !right {
832 io.out.push(Outbound::Dig {
834 status: DigStatus::ReleaseUseItem,
835 pos: BlockPos::new(0, 0, 0),
836 face: Face::Down,
837 });
838 self.using = false;
839 }
840 } else {
841 if left && !self.left_was {
842 self.click_mouse(t, io);
843 }
844 if right && !self.right_was {
845 self.right_click_mouse(t, io);
846 }
847 }
848 if right && self.right_click_delay == 0 && !self.using {
849 self.right_click_mouse(t, io);
850 }
851 self.send_click_block_to_controller(left, t, io);
852 self.left_was = left;
853 self.right_was = right;
854 }
855
856 fn click_mouse(&mut self, t: Option<BlockHit>, io: &mut HandsIo<'_>) {
857 if self.left_click_counter > 0 {
858 return;
859 }
860 if let Some(e) = io.entity {
861 if io.modern {
862 self.attack(e.id, io);
863 io.out.push(Outbound::Swing);
864 } else {
865 io.out.push(Outbound::Swing);
866 self.attack(e.id, io);
867 }
868 return;
869 }
870 if io.modern {
871 match t {
874 Some(hit) if io.solid(hit.pos).is_some() => {
875 self.click_block(hit.pos, hit.face, io);
876 }
877 _ => {
878 self.left_click_counter = 10;
879 self.ticks_since_swing = 0;
881 }
882 }
883 io.out.push(Outbound::Swing);
884 return;
885 }
886 io.out.push(Outbound::Swing);
887 match t {
888 Some(hit) => {
889 if io.solid(hit.pos).is_some() {
890 self.click_block(hit.pos, hit.face, io);
891 }
892 }
893 None => self.left_click_counter = 10,
894 }
895 }
896
897 fn send_click_block_to_controller(
898 &mut self,
899 left: bool,
900 t: Option<BlockHit>,
901 io: &mut HandsIo<'_>,
902 ) {
903 if !left {
904 self.left_click_counter = 0;
905 }
906 if self.left_click_counter > 0 || self.using {
907 return;
908 }
909 match t.filter(|_| io.entity.is_none()) {
910 Some(hit) if left => {
911 let solid = io.solid(hit.pos).is_some();
912 if solid && self.damage_block(hit.pos, hit.face, io) {
913 io.out.push(Outbound::Swing);
914 }
915 }
916 _ => self.reset_block_removing(io),
917 }
918 }
919
920 fn hitting_position(&self, pos: BlockPos, io: &HandsIo<'_>) -> bool {
921 self.hitting && self.current == Some(pos) && self.hit_item == io.held.map(|h| h.id)
922 }
923
924 pub(crate) fn click_block(&mut self, pos: BlockPos, face: Face, io: &mut HandsIo<'_>) {
926 if self.hitting && self.hitting_position(pos, io) {
927 return;
928 }
929 if self.hitting
930 && let Some(cur) = self.current
931 {
932 io.out.push(Outbound::Dig {
933 status: DigStatus::Abort,
934 pos: cur,
935 face,
936 });
937 }
938 io.out.push(Outbound::Dig {
939 status: DigStatus::Start,
940 pos,
941 face,
942 });
943 if let Some(b) = io.solid(pos)
944 && relative_hardness(&b, io.held.as_ref(), io.factors) >= 1.0
945 {
946 self.destroyed(pos, io);
947 return;
948 }
949 self.hitting = true;
950 self.current = Some(pos);
951 self.hit_item = io.held.map(|h| h.id);
952 self.damage = 0.0;
953 }
954
955 fn damage_block(&mut self, pos: BlockPos, face: Face, io: &mut HandsIo<'_>) -> bool {
957 if self.hit_delay > 0 {
958 self.hit_delay -= 1;
959 return true;
960 }
961 if !self.hitting_position(pos, io) {
962 self.click_block(pos, face, io);
963 return true;
964 }
965 let Some(b) = io.solid(pos) else {
966 self.hitting = false;
967 return false;
968 };
969 self.damage += relative_hardness(&b, io.held.as_ref(), io.factors);
970 if self.damage >= 1.0 {
971 self.hitting = false;
972 io.out.push(Outbound::Dig {
973 status: DigStatus::Finish,
974 pos,
975 face,
976 });
977 self.destroyed(pos, io);
978 self.damage = 0.0;
979 self.hit_delay = 5;
980 }
981 true
982 }
983
984 fn destroyed(&mut self, pos: BlockPos, io: &mut HandsIo<'_>) {
986 io.broken.push(pos);
987 if let Some(c) = self.current {
988 self.current = Some(BlockPos::new(c.x, -1, c.z));
989 }
990 self.broken += 1;
991 }
992
993 pub(crate) fn reset_block_removing(&mut self, io: &mut HandsIo<'_>) {
995 if !self.hitting {
996 return;
997 }
998 if let Some(cur) = self.current {
999 io.out.push(Outbound::Dig {
1000 status: DigStatus::Abort,
1001 pos: cur,
1002 face: Face::Down,
1003 });
1004 }
1005 self.hitting = false;
1006 self.damage = 0.0;
1007 }
1008
1009 fn right_click_mouse(&mut self, t: Option<BlockHit>, io: &mut HandsIo<'_>) {
1011 if self.hitting {
1012 return;
1013 }
1014 self.right_click_delay = 4;
1015 if io.modern {
1016 self.right_click_modern(t, io);
1017 return;
1018 }
1019 if let Some(e) = io.entity {
1020 self.interact(e, false, io);
1022 if let Some(h) = io.held {
1023 io.out.push(Outbound::UseItem {
1024 held: io.held_stack.clone(),
1025 off_hand: false,
1026 });
1027 if h.usable && (!h.food || h.always_edible || io.food < 20) {
1028 self.using = true;
1029 }
1030 }
1031 return;
1032 }
1033 let mut fall_through = true;
1034 if let Some(hit) = t
1035 && let Some(b) = io.solid(hit.pos)
1036 {
1037 if self.right_click_block(hit, &b, io) {
1038 fall_through = false;
1039 io.out.push(Outbound::Swing);
1040 }
1041 if io.held.is_none() {
1042 return;
1043 }
1044 }
1045 if fall_through && let Some(h) = io.held {
1046 io.out.push(Outbound::UseItem {
1048 held: io.held_stack.clone(),
1049 off_hand: false,
1050 });
1051 if h.usable && (!h.food || h.always_edible || io.food < 20) {
1053 self.using = true;
1054 }
1055 }
1056 }
1057
1058 fn right_click_modern(&mut self, t: Option<BlockHit>, io: &mut HandsIo<'_>) {
1061 let any_item = io.held.is_some() || io.off_held.is_some();
1062 for off_hand in [false, true] {
1063 let (held, stack) = if off_hand {
1064 (io.off_held, io.off_stack.clone())
1065 } else {
1066 (io.held, io.held_stack.clone())
1067 };
1068 let swing = if off_hand {
1069 Outbound::SwingOffHand
1070 } else {
1071 Outbound::Swing
1072 };
1073 if let Some(e) = io.entity {
1074 self.interact(e, off_hand, io);
1075 } else if let Some(hit) = t
1076 && let Some(b) = io.solid(hit.pos)
1077 {
1078 let cursor = Vec3::new(
1079 hit.hit.x - f64::from(hit.pos.x),
1080 hit.hit.y - f64::from(hit.pos.y),
1081 hit.hit.z - f64::from(hit.pos.z),
1082 );
1083 io.out.push(Outbound::Place {
1084 pos: hit.pos,
1085 face: hit.face,
1086 cursor,
1087 held: stack.clone(),
1088 off_hand,
1089 });
1090 if b.activates && !(io.sneaking && any_item) {
1092 io.out.push(swing);
1093 return;
1094 }
1095 if let Some(h) = held
1096 && h.places_block
1097 && h.count > 0
1098 {
1099 if !off_hand {
1100 *io.placed = true;
1101 }
1102 io.out.push(swing);
1103 return;
1104 }
1105 }
1106 if let Some(h) = held {
1107 io.out.push(Outbound::UseItem {
1108 held: stack,
1109 off_hand,
1110 });
1111 if h.usable && (!h.food || h.always_edible || io.food < 20) {
1112 self.using = true;
1113 return;
1114 }
1115 }
1116 }
1117 }
1118
1119 pub(crate) fn right_click_block(
1122 &mut self,
1123 hit: BlockHit,
1124 b: &BlockQuery,
1125 io: &mut HandsIo<'_>,
1126 ) -> bool {
1127 let cursor = Vec3::new(
1128 f64::from((hit.hit.x - f64::from(hit.pos.x)) as f32),
1129 f64::from((hit.hit.y - f64::from(hit.pos.y)) as f32),
1130 f64::from((hit.hit.z - f64::from(hit.pos.z)) as f32),
1131 );
1132 let activated = b.activates && (!io.sneaking || io.held.is_none());
1133 io.out.push(Outbound::Place {
1134 pos: hit.pos,
1135 face: hit.face,
1136 cursor,
1137 held: io.held_stack.clone(),
1138 off_hand: false,
1139 });
1140 if activated {
1141 return true;
1142 }
1143 match io.held {
1144 Some(h) if h.places_block && h.count > 0 => {
1145 *io.placed = true;
1146 true
1147 }
1148 _ => false,
1149 }
1150 }
1151}
1152
1153#[cfg(test)]
1154mod tests {
1155 use super::*;
1156
1157 fn stone() -> BlockQuery {
1158 BlockQuery {
1159 air: false,
1160 water: false,
1161 block: 1,
1162 hardness: 1.5,
1163 dig_material: Some(DigMaterial::Rock),
1164 harvest_tools: &[270, 274],
1165 selection: Some([0.0, 0.0, 0.0, 1.0, 1.0, 1.0]),
1166 stairs: None,
1167 activates: false,
1168 }
1169 }
1170
1171 #[test]
1172 fn hand_stone_time() {
1173 let f = DigFactors {
1176 on_ground: true,
1177 ..DigFactors::default()
1178 };
1179 let per_tick = relative_hardness(&stone(), None, f);
1180 let mut d = 0.0_f32;
1181 let mut n = 0;
1182 while d < 1.0 {
1183 d += per_tick;
1184 n += 1;
1185 }
1186 assert_eq!(n, 151);
1187 }
1188
1189 #[test]
1190 fn look_south_hits_block() {
1191 let look = look_vector(0.0, 0.0);
1192 assert!(look.z > 0.99 && look.x.abs() < 1e-3);
1193 let blocks = |x: i32, y: i32, z: i32| {
1194 Some(if (x, y, z) == (0, 1, 2) {
1195 stone()
1196 } else {
1197 BlockQuery {
1198 air: true,
1199 selection: None,
1200 ..stone()
1201 }
1202 })
1203 };
1204 let hit = pick(&blocks, Vec3::new(0.5, 1.62, 0.5), look, REACH).unwrap();
1205 assert_eq!(hit.pos, BlockPos::new(0, 1, 2));
1206 assert_eq!(hit.face, Face::North);
1207 assert!((hit.distance - 1.5).abs() < 1e-3);
1208 }
1209
1210 fn io_blocks(x: i32, y: i32, z: i32) -> Option<BlockQuery> {
1211 Some(if (x, y, z) == (0, 0, 0) {
1212 stone()
1213 } else {
1214 BlockQuery {
1215 air: true,
1216 selection: None,
1217 ..stone()
1218 }
1219 })
1220 }
1221
1222 fn kinds(out: &[Outbound]) -> Vec<&'static str> {
1223 out.iter()
1224 .map(|o| match o {
1225 Outbound::Place {
1226 off_hand: false, ..
1227 } => "place_main",
1228 Outbound::Place { off_hand: true, .. } => "place_off",
1229 Outbound::UseItem {
1230 off_hand: false, ..
1231 } => "use_main",
1232 Outbound::UseItem { off_hand: true, .. } => "use_off",
1233 Outbound::Swing => "swing",
1234 Outbound::SwingOffHand => "swing_off",
1235 Outbound::Dig { .. } => "dig",
1236 _ => "other",
1237 })
1238 .collect()
1239 }
1240
1241 #[test]
1244 fn modern_right_click_hands() {
1245 let hit = Some(BlockHit {
1246 pos: BlockPos::new(0, 0, 0),
1247 face: Face::Up,
1248 hit: Vec3::new(0.5, 1.0, 0.5),
1249 distance: 1.6,
1250 });
1251 let run = |held: Option<HeldItem>| {
1252 let mut h = Hands::default();
1253 let (mut out, mut broken, mut placed) = (Vec::new(), Vec::new(), false);
1254 let mut io = HandsIo {
1255 blocks: &io_blocks,
1256 target: hit,
1257 held,
1258 held_stack: None,
1259 food: 20,
1260 modern: true,
1261 off_held: None,
1262 off_stack: None,
1263 entity: None,
1264 factors: DigFactors::default(),
1265 sneaking: false,
1266 out: &mut out,
1267 broken: &mut broken,
1268 placed: &mut placed,
1269 };
1270 h.tick(false, true, &mut io);
1271 kinds(&out)
1272 };
1273 assert_eq!(run(None), ["place_main", "place_off"]);
1274 let dirt = HeldItem {
1275 id: 3,
1276 count: 10,
1277 places_block: true,
1278 ..HeldItem::default()
1279 };
1280 assert_eq!(run(Some(dirt)), ["place_main", "swing"]);
1281 let stick = HeldItem {
1283 id: 280,
1284 count: 1,
1285 ..HeldItem::default()
1286 };
1287 assert_eq!(run(Some(stick)), ["place_main", "use_main", "place_off"]);
1288 }
1289
1290 #[test]
1294 fn instant_break_press_starts_once() {
1295 fn instant(x: i32, y: i32, z: i32) -> Option<BlockQuery> {
1296 Some(if (x, y, z) == (0, 0, 0) {
1297 BlockQuery {
1298 hardness: 0.0,
1299 ..stone()
1300 }
1301 } else {
1302 BlockQuery {
1303 air: true,
1304 selection: None,
1305 ..stone()
1306 }
1307 })
1308 }
1309 let hit = Some(BlockHit {
1310 pos: BlockPos::new(0, 0, 0),
1311 face: Face::Up,
1312 hit: Vec3::new(0.5, 1.0, 0.5),
1313 distance: 1.6,
1314 });
1315 for modern in [false, true] {
1316 for pre_broken in [false, true] {
1317 let mut h = Hands::default();
1318 let (mut out, mut placed) = (Vec::new(), false);
1319 let mut broken = if pre_broken {
1320 vec![BlockPos::new(0, 0, 0)]
1321 } else {
1322 Vec::new()
1323 };
1324 let mut io = HandsIo {
1325 blocks: &instant,
1326 target: hit,
1327 held: None,
1328 held_stack: None,
1329 food: 20,
1330 modern,
1331 off_held: None,
1332 off_stack: None,
1333 entity: None,
1334 factors: DigFactors::default(),
1335 sneaking: false,
1336 out: &mut out,
1337 broken: &mut broken,
1338 placed: &mut placed,
1339 };
1340 h.tick(true, false, &mut io);
1341 let starts = out
1342 .iter()
1343 .filter(|o| {
1344 matches!(
1345 o,
1346 Outbound::Dig {
1347 status: DigStatus::Start,
1348 ..
1349 }
1350 )
1351 })
1352 .count();
1353 let want = usize::from(!pre_broken);
1354 assert_eq!(
1355 starts, want,
1356 "modern {modern} pre_broken {pre_broken}: {out:?}"
1357 );
1358 assert_eq!(h.broken, want as u64);
1359 }
1360 }
1361 }
1362
1363 #[test]
1364 fn entity_pick_rules() {
1365 let eyes = Vec3::new(0.5, 1.62, 0.5);
1366 let look = look_vector(0.0, 0.0); let at = |z: f64| {
1368 (
1369 7,
1370 [0.2, 1.0, z, 0.8, 2.0, z + 0.6],
1371 Vec3::new(0.5, 1.0, z + 0.3),
1372 )
1373 };
1374 let hit = pick_entity([at(2.5)].into_iter(), eyes, look, None, 0.1, None).unwrap();
1376 assert_eq!(hit.id, 7);
1377 assert!((hit.hit.z - 2.4).abs() < 1e-9, "{hit:?}");
1378 assert!(pick_entity([at(2.5)].into_iter(), eyes, look, Some(1.5), 0.1, None).is_none());
1380 assert!(pick_entity([at(3.6)].into_iter(), eyes, look, None, 0.0, None).is_none());
1382 let inside = (8, [0.0, 1.0, 0.0, 1.0, 2.0, 1.0], Vec3::new(0.5, 1.0, 0.5));
1384 assert_eq!(
1385 pick_entity([inside].into_iter(), eyes, look, None, 0.0, None).map(|h| h.distance),
1386 Some(0.0)
1387 );
1388 }
1389
1390 #[test]
1392 fn modern_left_click_order() {
1393 let hit = Some(BlockHit {
1394 pos: BlockPos::new(0, 0, 0),
1395 face: Face::Up,
1396 hit: Vec3::new(0.5, 1.0, 0.5),
1397 distance: 1.6,
1398 });
1399 for (modern, want) in [(true, ["dig", "swing"]), (false, ["swing", "dig"])] {
1400 let mut h = Hands::default();
1401 let (mut out, mut broken, mut placed) = (Vec::new(), Vec::new(), false);
1402 let mut io = HandsIo {
1403 blocks: &io_blocks,
1404 target: hit,
1405 held: None,
1406 held_stack: None,
1407 food: 20,
1408 modern,
1409 off_held: None,
1410 off_stack: None,
1411 entity: None,
1412 factors: DigFactors::default(),
1413 sneaking: false,
1414 out: &mut out,
1415 broken: &mut broken,
1416 placed: &mut placed,
1417 };
1418 h.click_mouse(hit, &mut io);
1419 assert_eq!(kinds(&out)[..2], want);
1420 }
1421 }
1422
1423 #[test]
1424 fn dig_sequence() {
1425 let blocks = |x: i32, y: i32, z: i32| {
1426 Some(if (x, y, z) == (0, 0, 0) {
1427 BlockQuery {
1428 hardness: 0.6,
1429 ..stone()
1430 }
1431 } else {
1432 BlockQuery {
1433 air: true,
1434 selection: None,
1435 ..stone()
1436 }
1437 })
1438 };
1439 let target = Some(BlockHit {
1440 pos: BlockPos::new(0, 0, 0),
1441 face: Face::Up,
1442 hit: Vec3::new(0.5, 1.0, 0.5),
1443 distance: 1.6,
1444 });
1445 let mut h = Hands::default();
1446 let mut out = Vec::new();
1447 let mut broken = Vec::new();
1448 let mut placed = false;
1449 let mut ticks = 0;
1450 while broken.is_empty() && ticks < 200 {
1451 let mut io = HandsIo {
1452 blocks: &blocks,
1453 target,
1454 held: None,
1455 held_stack: None,
1456 food: 20,
1457 modern: false,
1458 off_held: None,
1459 off_stack: None,
1460 entity: None,
1461 factors: DigFactors {
1462 on_ground: true,
1463 ..DigFactors::default()
1464 },
1465 sneaking: false,
1466 out: &mut out,
1467 broken: &mut broken,
1468 placed: &mut placed,
1469 };
1470 h.tick(true, false, &mut io);
1471 ticks += 1;
1472 }
1473 assert_eq!(broken, [BlockPos::new(0, 0, 0)]);
1476 let digs: Vec<_> = out
1477 .iter()
1478 .filter_map(|o| match o {
1479 Outbound::Dig { status, .. } => Some(*status),
1480 _ => None,
1481 })
1482 .collect();
1483 assert_eq!(digs, [DigStatus::Start, DigStatus::Finish]);
1484 assert_eq!(ticks, 61);
1485 assert_eq!(
1487 out.iter().filter(|o| matches!(o, Outbound::Swing)).count(),
1488 62
1489 );
1490 }
1491}