Merge pull request #2992 from kacpherek/fix/mob-ai-villager-parity
🤖 Agent: read the diff atefa8df83b("Villagers hear bells, use bone meal, fetch dropped items and play tag ") against the 26.3 source it cites; verify green, merges cleanly with main. Proof: cg25 union a051cc285 (local branch cg25/union in t3-ef805fdb: main6d3489d30+ 38 contributor heads merged cleanly, no fixups): clippy -p horizon-client --all-targets -D warnings clean, horizon-client lib 1105 + bin 133 + integration tests pass, parity --changed 355/355 harnesses PASS on the Mac mini (Cargo.lock changed by #3210, so every harness ran; tab_list passed on its one retry).
This commit is contained in:
@@ -435,7 +435,8 @@ pub struct BellTick {
|
||||
pub ended: Option<(Vec<([f64; 3], i32)>, Vec<u64>)>,
|
||||
}
|
||||
|
||||
fn closer_to_center_than(pos: [i32; 3], p: [f64; 3], distance: f64) -> bool {
|
||||
/// `Vec3i.closerToCenterThan`: `p` is within `distance` of the block's centre.
|
||||
pub fn closer_to_center_than(pos: [i32; 3], p: [f64; 3], distance: f64) -> bool {
|
||||
let dx = f64::from(pos[0]) + 0.5 - p[0];
|
||||
let dy = f64::from(pos[1]) + 0.5 - p[1];
|
||||
let dz = f64::from(pos[2]) + 0.5 - p[2];
|
||||
|
||||
@@ -7,6 +7,7 @@ use super::Game;
|
||||
use horizon_client::{
|
||||
block_entity_anim::{
|
||||
BellEntity, ConduitServer, ConduitTarget, EndGateway, Shulker, ShulkerStatus, Ticker,
|
||||
closer_to_center_than,
|
||||
},
|
||||
block_entity_render::BlockAnim,
|
||||
collisions::{SceneCollisions, no_collision_empty},
|
||||
@@ -49,11 +50,16 @@ impl Game<'_> {
|
||||
}
|
||||
Some(Ticker::Bell) => {
|
||||
let gathered = self.bell_gather(pos);
|
||||
self.be_anims
|
||||
.bells
|
||||
.entry(pos)
|
||||
.or_default()
|
||||
.trigger_event(a, b, time, || gathered);
|
||||
let rung =
|
||||
self.be_anims
|
||||
.bells
|
||||
.entry(pos)
|
||||
.or_default()
|
||||
.trigger_event(a, b, time, || gathered);
|
||||
// The server's copy tells the living entities within 32 blocks.
|
||||
if rung && self.remote.is_none() {
|
||||
self.bell_heard(pos, time);
|
||||
}
|
||||
}
|
||||
Some(Ticker::Gateway) => {
|
||||
let initial = self.gateway_loaded(pos);
|
||||
@@ -200,6 +206,26 @@ impl Game<'_> {
|
||||
}
|
||||
out
|
||||
}
|
||||
/// `BellBlockEntity.updateEntities`: every gathered entity alive within 32
|
||||
/// blocks of the bell's centre gets `HEARD_BELL_TIME` (only villagers
|
||||
/// have a brain that reads it).
|
||||
fn bell_heard(&mut self, pos: [i32; 3], now: i64) {
|
||||
let nearby = self
|
||||
.be_anims
|
||||
.bells
|
||||
.get(&pos)
|
||||
.and_then(|b| b.nearby.clone())
|
||||
.unwrap_or_default();
|
||||
for m in &mut self.mobs {
|
||||
if m.alive()
|
||||
&& !m.removed
|
||||
&& nearby.contains(&u64::from(m.id))
|
||||
&& closer_to_center_than(pos, [m.position.x, m.position.y, m.position.z], 32.0)
|
||||
{
|
||||
horizon_client::mobs::villager_brain::heard_bell(m, now);
|
||||
}
|
||||
}
|
||||
}
|
||||
/// `ShulkerBoxBlock.canOpen`: a closed box opens only when its lid has
|
||||
/// room to rise (`level.noCollision` of the half-open lid box).
|
||||
pub(super) fn shulker_can_open(&self, pos: [i32; 3]) -> bool {
|
||||
|
||||
@@ -745,6 +745,7 @@ impl Game<'_> {
|
||||
self.guardian_bubbles();
|
||||
self.villager_reputation_tick();
|
||||
self.merchant_tick();
|
||||
self.villager_item_sense();
|
||||
self.villager_pickups();
|
||||
for m in dead {
|
||||
self.mob_died(&m);
|
||||
@@ -1362,6 +1363,26 @@ impl Game<'_> {
|
||||
}
|
||||
}
|
||||
},
|
||||
MobEvent::Bonemeal { pos } => {
|
||||
let at = BlockPos::new(pos[0], pos[1], pos[2]);
|
||||
if self.with_level(|b, level, ticks| b.grow_crop(level, ticks, at)) {
|
||||
self.level_events
|
||||
.push(horizon_world::level::LevelEvent::LevelEvent {
|
||||
id: 1505,
|
||||
pos: at,
|
||||
data: 15,
|
||||
});
|
||||
let _ = self.process_level_events();
|
||||
}
|
||||
}
|
||||
MobEvent::RingBell { bell } => {
|
||||
let pos = BlockPos::new(bell[0], bell[1], bell[2]);
|
||||
let rang = self
|
||||
.with_level(|b, level, ticks| b.bell_attempt_to_ring(level, ticks, pos));
|
||||
if rang {
|
||||
let _ = self.process_level_events();
|
||||
}
|
||||
}
|
||||
MobEvent::ChestOpen { chest, open } => self.golem_chest_open(chest, open),
|
||||
MobEvent::ChestSlot { chest, slot, stack } => {
|
||||
self.golem_chest_slot(chest, slot, stack)
|
||||
|
||||
@@ -240,6 +240,67 @@ impl Game<'_> {
|
||||
}
|
||||
}
|
||||
|
||||
/// `NearestItemSensor.doTick` for the villagers whose sensor is due: the
|
||||
/// nearest dropped item within 32 blocks that `Villager.wantsToPickUp`
|
||||
/// accepts and the villager sees, or none.
|
||||
pub(super) fn villager_item_sense(&mut self) {
|
||||
if self.dropped.is_empty() {
|
||||
for m in &mut self.mobs {
|
||||
if m.species() == Species::Villager && villager_brain::take_item_scan(m) {
|
||||
villager_brain::set_wanted_item(m, None);
|
||||
}
|
||||
}
|
||||
return;
|
||||
}
|
||||
let picks_up = self
|
||||
.res
|
||||
.items()
|
||||
.item_tags
|
||||
.get("minecraft:villager_picks_up")
|
||||
.cloned();
|
||||
let mut due = Vec::new();
|
||||
for (i, m) in self.mobs.iter_mut().enumerate() {
|
||||
if m.species() == Species::Villager && villager_brain::take_item_scan(m) {
|
||||
due.push(i);
|
||||
}
|
||||
}
|
||||
if due.is_empty() {
|
||||
return;
|
||||
}
|
||||
let w = self.mob_world_base(&[]);
|
||||
let mut found = Vec::new();
|
||||
for i in due {
|
||||
let m = &self.mobs[i];
|
||||
let area = m.bounds().inflate(Vec3::new(32.0, 16.0, 32.0));
|
||||
let requested = villager::requested_items(m);
|
||||
let eye = Vec3::new(m.position.x, m.eye_y(), m.position.z);
|
||||
let mut best: Option<(f64, Vec3)> = None;
|
||||
for e in &self.dropped {
|
||||
let d = e.position.distance_squared(m.position);
|
||||
if best.is_some_and(|(b, _)| d >= b)
|
||||
|| !e.bounds().intersects(area)
|
||||
|| d >= 32.0 * 32.0
|
||||
{
|
||||
continue;
|
||||
}
|
||||
let item = &e.stack.item;
|
||||
let wants = villager::food_points(item) > 0
|
||||
|| picks_up.as_ref().is_some_and(|t| t.contains(&**item))
|
||||
|| requested.contains(&&**item);
|
||||
let fits =
|
||||
villager::ext(m).is_none_or(|x| x.can_add(&e.stack, self.item_max_stack(item)));
|
||||
// ItemEntity's eye height is 0.2125.
|
||||
let at = Vec3::new(e.position.x, e.position.y + 0.2125, e.position.z);
|
||||
if wants && fits && w.line_of_sight(eye, at) {
|
||||
best = Some((d, e.position));
|
||||
}
|
||||
}
|
||||
found.push((i, best.map(|(_, p)| [p.x, p.y, p.z])));
|
||||
}
|
||||
for (i, at) in found {
|
||||
villager_brain::set_wanted_item(&mut self.mobs[i], at);
|
||||
}
|
||||
}
|
||||
/// `Mob.aiStep` item pickup for villagers (`InventoryCarrier`): food,
|
||||
/// seeds and what their profession wants, from within a block.
|
||||
pub(super) fn villager_pickups(&mut self) {
|
||||
|
||||
@@ -980,6 +980,17 @@ pub enum MobEvent {
|
||||
count: u32,
|
||||
},
|
||||
Sound(&'static str, [f64; 3], f32, f32),
|
||||
/// `UseBonemeal`: the villager's bone meal grows the crop at `pos`
|
||||
/// (`BoneMealItem.growCrop`, then level event 1505).
|
||||
Bonemeal {
|
||||
pos: [i32; 3],
|
||||
},
|
||||
/// `RingBell`: the villager rings the meeting-point bell
|
||||
/// (`BellBlock.attemptToRing`: the bell shakes, the sound and the game
|
||||
/// event follow).
|
||||
RingBell {
|
||||
bell: [i32; 3],
|
||||
},
|
||||
/// `ChestBlockEntity.startOpen` / `stopOpen` by a copper golem: the
|
||||
/// chest's opener count goes up or down (interaction range 3).
|
||||
ChestOpen {
|
||||
|
||||
@@ -6,7 +6,7 @@ use super::{
|
||||
anim::{Poses, State},
|
||||
goals::{
|
||||
Cx, Goal, HurtByTarget, LOOK, LookAtPlayer, MOVE, MeleeAttack, NearestTarget, Prey,
|
||||
RandomLookAround, Stroll, goal_clone,
|
||||
RandomLookAround, Stroll, goal_clone, reduced,
|
||||
},
|
||||
};
|
||||
use crate::projectiles::{Projectile, ProjectileKind};
|
||||
@@ -158,7 +158,8 @@ impl Goal for OfferFlower {
|
||||
self.ticks > 0
|
||||
}
|
||||
fn start(&mut self, m: &mut Mob, cx: &mut Cx) {
|
||||
self.ticks = 400;
|
||||
// adjustedTickDelay(400): the goal ticks every other game tick.
|
||||
self.ticks = reduced(400);
|
||||
m.extra.counter = 400;
|
||||
// IronGolem.offerFlower(true): entity event 11.
|
||||
entity_event(m, 11, cx);
|
||||
|
||||
@@ -529,15 +529,14 @@ struct Beg {
|
||||
ticks: i32,
|
||||
}
|
||||
impl Beg {
|
||||
fn interesting(&self, m: &Mob, w: &MobWorld) -> bool {
|
||||
fn interesting(&self, w: &MobWorld) -> bool {
|
||||
let Some(p) = &w.player else {
|
||||
return false;
|
||||
};
|
||||
let held = |i: &Option<std::sync::Arc<str>>| {
|
||||
i.as_deref().is_some_and(|i| {
|
||||
(!m.extra.tame && i == "minecraft:bone")
|
||||
|| (m.extra.tame && self.food.iter().any(|f| f == i))
|
||||
})
|
||||
// `BegGoal.playerHoldingInteresting`: a bone or food, tame or not.
|
||||
i.as_deref()
|
||||
.is_some_and(|i| i == "minecraft:bone" || self.food.iter().any(|f| f == i))
|
||||
};
|
||||
p.alive && !p.spectator && (held(&p.main_hand) || held(&p.off_hand))
|
||||
}
|
||||
@@ -551,14 +550,14 @@ impl Goal for Beg {
|
||||
cx.w.player
|
||||
.as_ref()
|
||||
.is_some_and(|p| m.distance_sqr(p.position) <= 64.0)
|
||||
&& self.interesting(m, cx.w)
|
||||
&& self.interesting(cx.w)
|
||||
}
|
||||
fn can_continue(&mut self, m: &mut Mob, cx: &mut Cx) -> bool {
|
||||
cx.w.player
|
||||
.as_ref()
|
||||
.is_some_and(|p| p.alive && m.distance_sqr(p.position) <= 64.0)
|
||||
&& self.ticks > 0
|
||||
&& self.interesting(m, cx.w)
|
||||
&& self.interesting(cx.w)
|
||||
}
|
||||
fn start(&mut self, m: &mut Mob, _: &mut Cx) {
|
||||
m.extra.flag = true;
|
||||
|
||||
@@ -318,7 +318,11 @@ pub fn work_sound(profession: u8) -> Option<&'static str> {
|
||||
pub fn crop(w: &MobWorld, p: [i32; 3]) -> bool {
|
||||
matches!(
|
||||
w.block_name(p[0], p[1], p[2]),
|
||||
"minecraft:wheat" | "minecraft:carrots" | "minecraft:potatoes" | "minecraft:beetroots"
|
||||
"minecraft:wheat"
|
||||
| "minecraft:carrots"
|
||||
| "minecraft:potatoes"
|
||||
| "minecraft:beetroots"
|
||||
| "minecraft:torchflower_crop"
|
||||
)
|
||||
}
|
||||
/// `CropBlock.isMaxAge` for the block at `p`.
|
||||
@@ -326,6 +330,8 @@ pub fn ripe_crop(w: &MobWorld, p: [i32; 3]) -> bool {
|
||||
let max = match w.block_name(p[0], p[1], p[2]) {
|
||||
"minecraft:wheat" | "minecraft:carrots" | "minecraft:potatoes" => "7",
|
||||
"minecraft:beetroots" => "3",
|
||||
// TorchflowerCropBlock.getMaxAge is 2, past its age property (0-1):
|
||||
// the crop is never at max age (age 2 is the torchflower itself).
|
||||
_ => return false,
|
||||
};
|
||||
w.prop(p[0], p[1], p[2], "age") == Some(max)
|
||||
|
||||
@@ -204,6 +204,8 @@ pub struct Memories {
|
||||
pub nearest_hostile: Slot<Who>,
|
||||
pub hiding_place: Slot<[i32; 3]>,
|
||||
pub heard_bell_time: Slot<i64>,
|
||||
/// `NEAREST_VISIBLE_WANTED_ITEM`: where the item entity lies.
|
||||
pub nearest_wanted_item: Slot<[f64; 3]>,
|
||||
pub cant_reach_walk_target_since: Slot<i64>,
|
||||
pub golem_detected_recently: Slot<bool>,
|
||||
pub last_slept: Slot<i64>,
|
||||
@@ -236,6 +238,7 @@ impl Memories {
|
||||
self.nearest_hostile.tick();
|
||||
self.hiding_place.tick();
|
||||
self.heard_bell_time.tick();
|
||||
self.nearest_wanted_item.tick();
|
||||
self.cant_reach_walk_target_since.tick();
|
||||
self.golem_detected_recently.tick();
|
||||
self.last_slept.tick();
|
||||
@@ -322,6 +325,8 @@ pub struct Brain {
|
||||
baby: bool,
|
||||
/// Whether the nearest-bed sensor's batch cache is used (`NearestBedSensor`).
|
||||
bed_cache: Vec<([i32; 3], i64)>,
|
||||
/// `NearestItemSensor` is due; the game, which knows the items, answers.
|
||||
scan_items: bool,
|
||||
}
|
||||
#[derive(Clone, Debug)]
|
||||
struct Entry {
|
||||
@@ -362,6 +367,7 @@ impl Brain {
|
||||
shuffle_seed,
|
||||
baby,
|
||||
bed_cache: Vec::new(),
|
||||
scan_items: false,
|
||||
};
|
||||
b.list = packages(baby, shuffle_seed, farmer);
|
||||
b
|
||||
@@ -556,7 +562,7 @@ fn tick_sensors(m: &mut Mob, w: &MobWorld, lr: &mut LegacyRandom) {
|
||||
match i {
|
||||
0 => sense_living(m, w),
|
||||
1 => sense_players(m, w),
|
||||
2 => {}
|
||||
2 => brain_mut(m).scan_items = true,
|
||||
3 => sense_bed(m, w, lr),
|
||||
4 => sense_hurt(m, w),
|
||||
5 => sense_hostiles(m, w),
|
||||
@@ -1245,7 +1251,16 @@ enum B {
|
||||
valid: Vec<[i32; 3]>,
|
||||
worked: i32,
|
||||
},
|
||||
UseBonemeal(Timed),
|
||||
UseBonemeal {
|
||||
t: Timed,
|
||||
next_work: i64,
|
||||
last_session: i64,
|
||||
worked: i32,
|
||||
crop: Option<[i32; 3]>,
|
||||
/// The next crop is picked on the next tick, when the level shows the
|
||||
/// age the meal gave (vanilla grows the crop at once).
|
||||
repick: bool,
|
||||
},
|
||||
WalkToBlockMemory {
|
||||
running: bool,
|
||||
which: PoiMem,
|
||||
@@ -1347,13 +1362,13 @@ impl B {
|
||||
| B::GoToPotentialJobSite(t)
|
||||
| B::DoNothing(t)
|
||||
| B::TradeWithVillager(t)
|
||||
| B::UseBonemeal(t)
|
||||
| B::Celebrate(t) => t.running,
|
||||
B::MoveToTargetSink { t, .. }
|
||||
| B::GiveGiftToHero { t, .. }
|
||||
| B::ShowTrades { t, .. }
|
||||
| B::WorkAtPoi { t, .. }
|
||||
| B::HarvestFarmland { t, .. }
|
||||
| B::UseBonemeal { t, .. }
|
||||
| B::JumpOnBed { t, .. }
|
||||
| B::MakeLove { t, .. }
|
||||
| B::SleepInBed { t, .. } => t.running,
|
||||
@@ -1679,17 +1694,38 @@ impl B {
|
||||
}
|
||||
true
|
||||
}
|
||||
B::UseBonemeal(t) => {
|
||||
B::UseBonemeal {
|
||||
t,
|
||||
next_work,
|
||||
last_session,
|
||||
worked,
|
||||
crop,
|
||||
repick,
|
||||
} => {
|
||||
let mm = mem(m);
|
||||
if mm.look_target.present() || mm.walk_target.present() {
|
||||
return false;
|
||||
}
|
||||
// Bone meal in the inventory is needed; checked every 10 ticks.
|
||||
let has = super::villager::ext(m)
|
||||
.is_some_and(|e| e.count_item("minecraft:bone_meal") > 0);
|
||||
let _ = t;
|
||||
let _ = has;
|
||||
false
|
||||
// Checked every 10 ticks, and not within 160 ticks of the last session.
|
||||
let age = i64::from(m.age);
|
||||
if age % 10 != 0 || (*last_session != 0 && *last_session + 160 > age) {
|
||||
return false;
|
||||
}
|
||||
if super::villager::ext(m).is_none_or(|e| e.count_item("minecraft:bone_meal") == 0)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
*crop = pick_bonemeal_crop(m, cx);
|
||||
if crop.is_none() {
|
||||
return false;
|
||||
}
|
||||
t.begin(cx.lr, cx.now);
|
||||
bonemeal_target(m, *crop);
|
||||
m.extra.main_hand = Some("minecraft:bone_meal".into());
|
||||
*next_work = cx.now;
|
||||
*worked = 0;
|
||||
*repick = false;
|
||||
true
|
||||
}
|
||||
// -- OneShots.
|
||||
B::InteractWithDoor {
|
||||
@@ -1824,7 +1860,32 @@ impl B {
|
||||
},
|
||||
r,
|
||||
),
|
||||
B::GoToWantedItem(_) => false,
|
||||
B::GoToWantedItem(r) => one(
|
||||
|| {
|
||||
let mm = mem(m);
|
||||
if mm.walk_target.present() {
|
||||
return false;
|
||||
}
|
||||
let Some(at) = mm.nearest_wanted_item.value() else {
|
||||
return false;
|
||||
};
|
||||
// item.closerThan(body, 4); a villager can pick up loot.
|
||||
if m.position.distance_squared(Vec3::new(at[0], at[1], at[2])) >= 16.0 {
|
||||
return false;
|
||||
}
|
||||
let mm = mem(m);
|
||||
// The item's eye height is 0.2125.
|
||||
mm.look_target
|
||||
.set(Tracker::Point([at[0], at[1] + 0.2125, at[2]]));
|
||||
mm.walk_target.set(WalkTarget {
|
||||
target: Tracker::Point(at),
|
||||
speed: 0.5,
|
||||
close: 0,
|
||||
});
|
||||
true
|
||||
},
|
||||
r,
|
||||
),
|
||||
B::AcquirePoi {
|
||||
running,
|
||||
filter,
|
||||
@@ -2494,16 +2555,7 @@ impl B {
|
||||
if dist_sqr(bell, block_of(m.position)) < 9
|
||||
&& cx.w.block_name(bell[0], bell[1], bell[2]) == "minecraft:bell"
|
||||
{
|
||||
cx.events.push(MobEvent::SoundId(
|
||||
"minecraft:block.bell.use".into(),
|
||||
[
|
||||
f64::from(bell[0]) + 0.5,
|
||||
f64::from(bell[1]) + 0.5,
|
||||
f64::from(bell[2]) + 0.5,
|
||||
],
|
||||
2.0,
|
||||
1.0,
|
||||
));
|
||||
cx.events.push(MobEvent::RingBell { bell });
|
||||
}
|
||||
true
|
||||
},
|
||||
@@ -2649,17 +2701,61 @@ impl B {
|
||||
if cx.lr.next_int_bound(10).unwrap_or(0) != 0 {
|
||||
return false;
|
||||
}
|
||||
// Who chases whom is in the other kids' brains: chase the
|
||||
// first friend.
|
||||
if let Some(&kid) = friends.first() {
|
||||
let mm = mem(m);
|
||||
mm.interaction_target.set(kid);
|
||||
mm.look_target.set(Tracker::Entity(kid, true));
|
||||
mm.walk_target.set(WalkTarget {
|
||||
target: Tracker::Entity(kid, false),
|
||||
speed: 0.6,
|
||||
close: 1,
|
||||
});
|
||||
// What a friend chases is its INTERACTION_TARGET.
|
||||
let chasing = |who: Who| match who {
|
||||
Who::Mob(id) => {
|
||||
cx.w.seen(id)
|
||||
.and_then(|s| s.villager.as_ref())
|
||||
.and_then(|v| v.interaction_target)
|
||||
}
|
||||
Who::Player => None,
|
||||
};
|
||||
let me = Who::Mob(m.id);
|
||||
if !friends.iter().any(|&f| chasing(f) == Some(me)) {
|
||||
// The kid chased by the fewest (one to five) chasers, else any friend.
|
||||
let mut chased: Vec<(Who, i32)> = Vec::new();
|
||||
for &f in &friends {
|
||||
if let Some(c) = chasing(f) {
|
||||
match chased.iter_mut().find(|(k, _)| *k == c) {
|
||||
Some(e) => e.1 += 1,
|
||||
None => chased.push((c, 1)),
|
||||
}
|
||||
}
|
||||
}
|
||||
let target = chased
|
||||
.iter()
|
||||
.filter(|(_, n)| *n > 0 && *n <= 5)
|
||||
.min_by_key(|(_, n)| *n)
|
||||
.map(|&(k, _)| k)
|
||||
.or_else(|| friends.first().copied());
|
||||
if let Some(kid) = target {
|
||||
let mm = mem(m);
|
||||
mm.interaction_target.set(kid);
|
||||
mm.look_target.set(Tracker::Entity(kid, true));
|
||||
mm.walk_target.set(WalkTarget {
|
||||
target: Tracker::Entity(kid, false),
|
||||
speed: 0.6,
|
||||
close: 1,
|
||||
});
|
||||
}
|
||||
} else {
|
||||
// Chased: run to a spot inside the village.
|
||||
let villages = village_sections(m, cx.w);
|
||||
for _ in 0..10 {
|
||||
let Some(p) = m.land_random_pos(cx.w, 20, 8) else {
|
||||
continue;
|
||||
};
|
||||
let at = [
|
||||
p[0].floor() as i32,
|
||||
p[1].floor() as i32,
|
||||
p[2].floor() as i32,
|
||||
];
|
||||
let section = [at[0] >> 4, at[1] >> 4, at[2] >> 4];
|
||||
if sections_to_village(&villages, section) <= 1 {
|
||||
mem(m).walk_target.set(WalkTarget::point(p, 0.6, 0));
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
true
|
||||
},
|
||||
@@ -2974,7 +3070,45 @@ impl B {
|
||||
});
|
||||
}
|
||||
}
|
||||
B::UseBonemeal(_) => self.do_stop(m, cx),
|
||||
B::UseBonemeal {
|
||||
t,
|
||||
next_work,
|
||||
worked,
|
||||
crop,
|
||||
repick,
|
||||
..
|
||||
} => {
|
||||
if *repick {
|
||||
*repick = false;
|
||||
*crop = pick_bonemeal_crop(m, cx);
|
||||
bonemeal_target(m, *crop);
|
||||
}
|
||||
// canStillUse: fewer than 80 work cycles and a crop to work on.
|
||||
if now > t.end || *worked >= 80 || crop.is_none() {
|
||||
self.do_stop(m, cx);
|
||||
return;
|
||||
}
|
||||
let Some(p) = *crop else { return };
|
||||
if now >= *next_work && closer_to_center(p, m.position, 1.0) {
|
||||
let e = super::villager::ext_mut(m);
|
||||
// BoneMealItem.growCrop: a CropBlock below max age takes the meal.
|
||||
if let Some(slot) = e
|
||||
.inventory
|
||||
.iter()
|
||||
.position(|s| s.count > 0 && &*s.item == "minecraft:bone_meal")
|
||||
&& bonemeal_valid(cx.w, p)
|
||||
{
|
||||
e.inventory[slot].count -= 1;
|
||||
if e.inventory[slot].count == 0 {
|
||||
e.inventory.remove(slot);
|
||||
}
|
||||
cx.events.push(MobEvent::Bonemeal { pos: p });
|
||||
*repick = true;
|
||||
*next_work = now + 40;
|
||||
}
|
||||
*worked += 1;
|
||||
}
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
@@ -3062,7 +3196,13 @@ impl B {
|
||||
t.running = false;
|
||||
mem(m).breed_target.clear();
|
||||
}
|
||||
B::UseBonemeal(t) => t.running = false,
|
||||
B::UseBonemeal {
|
||||
t, last_session, ..
|
||||
} => {
|
||||
t.running = false;
|
||||
m.extra.main_hand = None;
|
||||
*last_session = i64::from(m.age);
|
||||
}
|
||||
B::WorkAtPoi { t, .. } => t.running = false,
|
||||
B::HarvestFarmland {
|
||||
t, next_ok, worked, ..
|
||||
@@ -3330,6 +3470,39 @@ fn harvest_tick(
|
||||
}
|
||||
}
|
||||
}
|
||||
/// `UseBonemeal.validPos`: a `CropBlock` below its max age.
|
||||
fn bonemeal_valid(w: &MobWorld, p: [i32; 3]) -> bool {
|
||||
super::villager::crop(w, p) && !super::villager::ripe_crop(w, p)
|
||||
}
|
||||
/// `UseBonemeal.pickNextTarget`: one of the valid blocks around the villager,
|
||||
/// each equally likely (reservoir sampling with the level random).
|
||||
fn pick_bonemeal_crop(m: &Mob, cx: &mut Bx) -> Option<[i32; 3]> {
|
||||
let c = block_of(m.position);
|
||||
let mut result = None;
|
||||
let mut count = 0;
|
||||
for x in -1..=1 {
|
||||
for y in -1..=1 {
|
||||
for z in -1..=1 {
|
||||
let p = [c[0] + x, c[1] + y, c[2] + z];
|
||||
if bonemeal_valid(cx.w, p) {
|
||||
count += 1;
|
||||
if cx.lr.next_int_bound(count).unwrap_or(1) == 0 {
|
||||
result = Some(p);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
result
|
||||
}
|
||||
/// `UseBonemeal.setCurrentCropAsTarget`.
|
||||
fn bonemeal_target(m: &mut Mob, crop: Option<[i32; 3]>) {
|
||||
if let Some(p) = crop {
|
||||
let mm = mem(m);
|
||||
mm.look_target.set(Tracker::Block(p));
|
||||
mm.walk_target.set(WalkTarget::block(p, 0.5, 1));
|
||||
}
|
||||
}
|
||||
/// The remembered POI positions a behaviour reads.
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
||||
enum PoiMem {
|
||||
@@ -3825,7 +3998,17 @@ fn packages(baby: bool, seed: i64, farmer: bool) -> Vec<Entry> {
|
||||
},
|
||||
if farmer { 2 } else { 5 },
|
||||
),
|
||||
(B::UseBonemeal(timed(60, 60)), if farmer { 4 } else { 7 }),
|
||||
(
|
||||
B::UseBonemeal {
|
||||
t: timed(60, 60),
|
||||
next_work: 0,
|
||||
last_session: 0,
|
||||
worked: 0,
|
||||
crop: None,
|
||||
repick: false,
|
||||
},
|
||||
if farmer { 4 } else { 7 },
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
@@ -4208,6 +4391,21 @@ pub fn woken(m: &mut Mob, now: i64) {
|
||||
mem(m).last_woken.set(now);
|
||||
}
|
||||
}
|
||||
/// `BellBlockEntity.updateEntities`: a bell rung within 32 blocks sets
|
||||
/// `HEARD_BELL_TIME` to the game time.
|
||||
pub fn heard_bell(m: &mut Mob, now: i64) {
|
||||
if brain(m).is_some() {
|
||||
mem(m).heard_bell_time.set(now);
|
||||
}
|
||||
}
|
||||
/// Whether `NearestItemSensor` is due for this villager (it is then reset).
|
||||
pub fn take_item_scan(m: &mut Mob) -> bool {
|
||||
brain(m).is_some() && std::mem::take(&mut brain_mut(m).scan_items)
|
||||
}
|
||||
/// `NearestItemSensor.doTick`: the nearest wanted item in sight, or none.
|
||||
pub fn set_wanted_item(m: &mut Mob, at: Option<[f64; 3]>) {
|
||||
mem(m).nearest_wanted_item.set_opt(at);
|
||||
}
|
||||
/// What other villagers see of this one's brain.
|
||||
pub fn potential_job_site(m: &Mob) -> Option<[i32; 3]> {
|
||||
mem_ref(m).and_then(|mm| mm.potential_job_site.value())
|
||||
|
||||
@@ -844,6 +844,23 @@ impl Behaviours {
|
||||
self.data(state).is(classes::Class::Bell)
|
||||
&& sim.bell_hit(pos, state, face, hit_y - f64::from(pos.y))
|
||||
}
|
||||
/// `BellBlock.attemptToRing(entity, level, pos, null)`: a mob rings the
|
||||
/// bell from its facing. False when the block at `pos` is not a bell.
|
||||
pub fn bell_attempt_to_ring<L: LevelAccess + ?Sized>(
|
||||
&self,
|
||||
level: &mut L,
|
||||
ticks: &mut Ticks,
|
||||
pos: BlockPos,
|
||||
) -> bool {
|
||||
let mut sim = self.sim(level, ticks);
|
||||
let state = sim.state(pos);
|
||||
if !self.data(state).is(classes::Class::Bell) {
|
||||
return false;
|
||||
}
|
||||
let facing = sim.b.facing(state);
|
||||
sim.bell_ring(pos, state, facing);
|
||||
true
|
||||
}
|
||||
/// Redstone signal queries (`SignalGetter`).
|
||||
pub fn has_neighbor_signal<L: LevelAccess + ?Sized>(
|
||||
&self,
|
||||
|
||||
Reference in New Issue
Block a user