mirror of
https://github.com/horizon-rust/horizon.git
synced 2026-11-07 11:06:11 -10:00
Merge pull request #11670 from horizon-rust/fix/breeding-bottle-xp-11388
🤖 Agent: breeding now adds one fresh orb of the whole 1-7 roll (no award split, no merge), and XP bottle orbs start 0.25 along the hit face's normal with their motion turned away from it; orb_lava/orb_water/orb_saved_uuid/bed_explosion harnesses passed.
This commit is contained in:
4 files changed
+129
-18
No files matched your search
@@ -14500,12 +14500,19 @@ impl Game<'_> {
|
||||
/// `ExperienceOrb.award`: orbs of the largest sizes; each draws the id
|
||||
/// `tryMergeToExisting` compares from the level random, then moves with
|
||||
/// its own random.
|
||||
fn spawn_orbs(&mut self, at: [f64; 3], mut value: u32) {
|
||||
fn spawn_orbs(&mut self, at: [f64; 3], value: u32) {
|
||||
self.spawn_orbs_toward(at, [0.0; 3], value);
|
||||
}
|
||||
/// `ExperienceOrb.awardWithDirection`: `award` whose fresh orbs start
|
||||
/// `roughly` that way (`new ExperienceOrb(level, pos, roughly, value)`).
|
||||
fn spawn_orbs_toward(&mut self, at: [f64; 3], roughly: [f64; 3], mut value: u32) {
|
||||
while value > 0 {
|
||||
let v = ExperienceOrb::split(value);
|
||||
value -= v;
|
||||
let merge_id = self.world.random.next_int_bound(40).unwrap_or(0);
|
||||
self.award_orb(at, v, merge_id);
|
||||
if !self.merge_new_orb(at, v, merge_id) {
|
||||
self.add_orb_toward(at, roughly, v);
|
||||
}
|
||||
}
|
||||
}
|
||||
/// `ExperienceOrb.tryMergeToExisting`: an orb of the same value within a
|
||||
@@ -14513,6 +14520,12 @@ impl Game<'_> {
|
||||
/// the new one as one more in its count (and restarts its age); otherwise
|
||||
/// a fresh orb spawns.
|
||||
fn award_orb(&mut self, at: [f64; 3], value: u32, merge_id: i32) {
|
||||
if !self.merge_new_orb(at, value, merge_id) {
|
||||
self.add_orb(at, value);
|
||||
}
|
||||
}
|
||||
/// `tryMergeToExisting`: true when an orb took the new one.
|
||||
fn merge_new_orb(&mut self, at: [f64; 3], value: u32, merge_id: i32) -> bool {
|
||||
let centre = Vec3::new(at[0], at[1], at[2]);
|
||||
let half = Vec3::new(0.5, 0.5, 0.5);
|
||||
let area = Aabb::new(centre.subtract(half), centre.add_vector(half));
|
||||
@@ -14534,8 +14547,9 @@ impl Game<'_> {
|
||||
if let Some(i) = target {
|
||||
self.orbs[i].count += 1;
|
||||
self.orbs[i].age = 0;
|
||||
true
|
||||
} else {
|
||||
self.add_orb(at, value);
|
||||
false
|
||||
}
|
||||
}
|
||||
/// Whether orb `into` takes orb `from` (0: a spawning orb of `from_value`),
|
||||
@@ -14609,10 +14623,33 @@ impl Game<'_> {
|
||||
/// `new ExperienceOrb(level, pos, Vec3.ZERO, value)`: yaw and motion from
|
||||
/// the orb's own random.
|
||||
fn add_orb(&mut self, at: [f64; 3], value: u32) {
|
||||
let at = self.unstuck_orb(at);
|
||||
self.add_orb_toward(at, [0.0; 3], value);
|
||||
}
|
||||
/// `new ExperienceOrb(level, pos, roughly, value)`: a random motion that
|
||||
/// points against `roughly` is turned round, and the orb starts half its
|
||||
/// size (0.25) along `roughly`, unstuck if it collides there.
|
||||
fn add_orb_toward(&mut self, at: [f64; 3], roughly: [f64; 3], value: u32) {
|
||||
let len_sqr = roughly.iter().map(|v| v * v).sum::<f64>();
|
||||
// Vec3.normalize: a vector shorter than 1e-5 has no direction.
|
||||
let len = len_sqr.sqrt();
|
||||
let unit = if len < 1.0e-5 {
|
||||
[0.0; 3]
|
||||
} else {
|
||||
roughly.map(|v| v / len)
|
||||
};
|
||||
let at = self.unstuck_orb([0, 1, 2].map(|a| at[a] + unit[a] * 0.25));
|
||||
let mut r = dispenser::entity_random();
|
||||
let _yaw = r.next_float();
|
||||
let p = Vec3::new(at[0], at[1], at[2]);
|
||||
let mut velocity = Vec3::new(
|
||||
(r.next_double() * 0.2 - 0.1) * 2.0,
|
||||
r.next_double() * 0.2 * 2.0,
|
||||
(r.next_double() * 0.2 - 0.1) * 2.0,
|
||||
);
|
||||
let dot = roughly[0] * velocity.x + roughly[1] * velocity.y + roughly[2] * velocity.z;
|
||||
if len_sqr > 0.0 && dot < 0.0 {
|
||||
velocity = Vec3::new(-velocity.x, -velocity.y, -velocity.z);
|
||||
}
|
||||
self.next_entity += 1;
|
||||
self.orbs.push(ExperienceOrb {
|
||||
rotation: [0.0, 0.0],
|
||||
@@ -14621,11 +14658,7 @@ impl Game<'_> {
|
||||
id: self.next_entity,
|
||||
position: p,
|
||||
previous: p,
|
||||
velocity: Vec3::new(
|
||||
(r.next_double() * 0.2 - 0.1) * 2.0,
|
||||
r.next_double() * 0.2 * 2.0,
|
||||
(r.next_double() * 0.2 - 0.1) * 2.0,
|
||||
),
|
||||
velocity,
|
||||
value,
|
||||
age: 0,
|
||||
on_ground: false,
|
||||
|
||||
@@ -3470,10 +3470,11 @@ impl Game<'_> {
|
||||
}
|
||||
}
|
||||
/// `Animal.finalizeSpawnChildFromBreeding`'s experience orb: only when the
|
||||
/// `mob_drops` game rule is on.
|
||||
/// `mob_drops` game rule is on, and one `new ExperienceOrb` of the whole
|
||||
/// value (not split or merged as `ExperienceOrb.award` does).
|
||||
fn spawn_breeding_orb(&mut self, at: [f64; 3], amount: u32) {
|
||||
if self.ui.rules.bool("mob_drops") {
|
||||
self.spawn_orbs(at, amount);
|
||||
self.add_orb(at, amount);
|
||||
}
|
||||
}
|
||||
/// `ServerPlayer.tellNeutralMobsThatIDied`: the mobs within 32 blocks
|
||||
@@ -3834,13 +3835,14 @@ impl Game<'_> {
|
||||
self.breeding_orb(parent, [p.position.x, p.position.y, p.position.z]);
|
||||
}
|
||||
/// `Animal.finalizeSpawnChildFromBreeding`'s experience, from the breeding
|
||||
/// parent's own random, and only when the `mob_drops` game rule is on.
|
||||
/// parent's own random, and only when the `mob_drops` game rule is on: one
|
||||
/// fresh orb of 1 to 7 (#11388).
|
||||
fn breeding_orb(&mut self, parent: u32, at: [f64; 3]) {
|
||||
if self.ui.rules.bool("mob_drops")
|
||||
&& let Some(m) = self.mobs.iter_mut().find(|m| m.id == parent)
|
||||
{
|
||||
let xp = 1 + m.random.next_int_bound(7).unwrap_or(0) as u32;
|
||||
self.spawn_orbs(at, xp);
|
||||
self.add_orb(at, xp);
|
||||
}
|
||||
}
|
||||
/// `MobEffectUtil.addEffectToPlayersAround` for an elder guardian:
|
||||
@@ -5719,7 +5721,7 @@ impl Game<'_> {
|
||||
_ => None,
|
||||
};
|
||||
let owner = self.potion_owner(p.owner);
|
||||
self.splash(stack, at, mob, block, p.age, &owner);
|
||||
self.splash(stack, at, mob, block, p.age, &owner, p.velocity);
|
||||
if stack.item.as_ref() != "minecraft:experience_bottle" {
|
||||
let item = stack.item.to_string();
|
||||
self.native_potion_splash(&item, [at.x, at.y, at.z]);
|
||||
@@ -9102,3 +9104,32 @@ mod pickable_projectile_tests {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod breeding_orb_tests {
|
||||
use super::*;
|
||||
|
||||
/// `Animal.finalizeSpawnChildFromBreeding`: one fresh orb of the parent's
|
||||
/// 1-to-7 roll, never split (4 is not 3 + 1) or merged (#11388).
|
||||
#[test]
|
||||
#[ignore = "needs prepared resources (HORIZON_TEST_RESOURCES)"]
|
||||
fn breeding_drops_one_orb_of_the_whole_roll() {
|
||||
let mut game = super::super::native_machines::tests::server_game();
|
||||
let kind = game.res.mob_types().get("minecraft:cow").unwrap().clone();
|
||||
game.mobs
|
||||
.push(Mob::new(7, kind, Vec3::new(8.5, 64.0, 8.5), 0.0));
|
||||
let mut values = std::collections::BTreeSet::new();
|
||||
for _ in 0..40 {
|
||||
let roll = 1 + game.mobs[0].random.clone().next_int_bound(7).unwrap() as u32;
|
||||
let orbs = game.orbs.len();
|
||||
game.breeding_orb(7, [8.5, 64.0, 8.5]);
|
||||
assert_eq!(game.orbs.len(), orbs + 1, "a fresh orb, not merged");
|
||||
assert_eq!(
|
||||
game.orbs.last().map(|o| (o.value, o.count)),
|
||||
Some((roll, 1))
|
||||
);
|
||||
values.insert(roll);
|
||||
}
|
||||
assert!(values.iter().any(|v| matches!(v, 4..=6)), "{values:?}");
|
||||
}
|
||||
}
|
||||
@@ -1361,6 +1361,7 @@ impl Game<'_> {
|
||||
t.age,
|
||||
t.owner.clone(),
|
||||
t.guest,
|
||||
t.velocity,
|
||||
));
|
||||
return false;
|
||||
}
|
||||
@@ -1388,7 +1389,7 @@ impl Game<'_> {
|
||||
);
|
||||
}
|
||||
}
|
||||
for (stack, at, mob, block, age, owner, guest) in landed {
|
||||
for (stack, at, mob, block, age, owner, guest, motion) in landed {
|
||||
// Projectile.onHit (super.onHit of AbstractThrownPotion): a block
|
||||
// is heard at its centre, an entity at the hit.
|
||||
let heard = if mob.is_some() {
|
||||
@@ -1406,7 +1407,7 @@ impl Game<'_> {
|
||||
attacker: None,
|
||||
guest,
|
||||
};
|
||||
self.splash(&stack, at, mob, block, age, &owner);
|
||||
self.splash(&stack, at, mob, block, age, &owner, motion);
|
||||
if item != "minecraft:experience_bottle" {
|
||||
self.native_potion_splash(&item, [at.x, at.y, at.z]);
|
||||
}
|
||||
@@ -1910,7 +1911,7 @@ impl Game<'_> {
|
||||
/// `AbstractThrownPotion.onHit` with `ThrownSplashPotion.onHitAsPotion`, or
|
||||
/// `ThrownLingeringPotion.onHitAsPotion` for a lingering potion, read from
|
||||
/// the stack's `potion_contents`; `mob` is where it hit a mob, `hit` the
|
||||
/// block and face it hit, `age` its age.
|
||||
/// block and face it hit, `age` its age, `motion` its motion as it hit.
|
||||
pub(super) fn splash(
|
||||
&mut self,
|
||||
stack: &ItemStack,
|
||||
@@ -1919,6 +1920,7 @@ impl Game<'_> {
|
||||
hit: Option<([i32; 3], Direction)>,
|
||||
age: u32,
|
||||
owner: &PotionOwner,
|
||||
motion: Vec3,
|
||||
) {
|
||||
let block = [at.x, at.y, at.z].map(|v| v.floor() as i32);
|
||||
if &*stack.item == "minecraft:experience_bottle" {
|
||||
@@ -1928,7 +1930,13 @@ impl Game<'_> {
|
||||
self.level_event_at(1053, block, 0);
|
||||
let r = &mut self.world.random;
|
||||
let value = 3 + r.next_int_bound(5).unwrap_or(0) + r.next_int_bound(5).unwrap_or(0);
|
||||
self.spawn_orbs([at.x, at.y, at.z], value as u32);
|
||||
// awardWithDirection: away from the face it hit, or back along
|
||||
// its own motion when it hit an entity (#11388).
|
||||
let roughly = match hit {
|
||||
Some((_, face)) => face.normal().map(f64::from),
|
||||
None => [-motion.x, -motion.y, -motion.z],
|
||||
};
|
||||
self.spawn_orbs_toward([at.x, at.y, at.z], roughly, value as u32);
|
||||
self.native_exp_bottle([at.x, at.y, at.z], value as u32);
|
||||
return;
|
||||
}
|
||||
@@ -2496,3 +2504,40 @@ mod sound_on_added_tests {
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod xp_bottle_tests {
|
||||
use super::*;
|
||||
|
||||
/// `ThrownExperienceBottle.onHit` uses `awardWithDirection`: every fresh
|
||||
/// orb starts 0.25 out from the face it hit, or back along the bottle's
|
||||
/// motion after an entity hit, moving that way (#11388).
|
||||
#[test]
|
||||
#[ignore = "needs prepared resources (HORIZON_TEST_RESOURCES)"]
|
||||
fn bottle_orbs_start_away_from_what_it_hit() {
|
||||
let mut game = super::super::native_machines::tests::server_game();
|
||||
let bottle = ItemStack::new("minecraft:experience_bottle", 1);
|
||||
let at = Vec3::new(8.5, 70.5, 8.5);
|
||||
// (hit face, the bottle's motion, the axis and sign the orbs leave by)
|
||||
for (face, motion, axis, sign) in [
|
||||
(
|
||||
Some(([9, 70, 8], Direction::West)),
|
||||
Vec3::new(1.0, 0.0, 0.0),
|
||||
0,
|
||||
-1.0,
|
||||
),
|
||||
(None, Vec3::new(0.0, 0.0, 1.0), 2, -1.0),
|
||||
] {
|
||||
game.orbs.clear();
|
||||
game.splash(&bottle, at, None, face, 0, &PotionOwner::default(), motion);
|
||||
assert!(!game.orbs.is_empty());
|
||||
let at = [at.x, at.y, at.z];
|
||||
for o in game.orbs.iter() {
|
||||
let p = [o.position.x, o.position.y, o.position.z];
|
||||
let v = [o.velocity.x, o.velocity.y, o.velocity.z];
|
||||
assert!((p[axis] - (at[axis] + sign * 0.25)).abs() < 1e-9, "{p:?}");
|
||||
assert!(v[axis] * sign >= 0.0, "{v:?}");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -52,6 +52,7 @@ impl Game {
|
||||
fn add_orb(&mut self, _: [f64; 3], value: u32) { self.added.push(value); }
|
||||
ORB_MERGES
|
||||
AWARD_ORB
|
||||
MERGE_NEW_ORB
|
||||
MERGE_ORBS
|
||||
}
|
||||
fn orb(id: u32, x: f64, value: u32, ticks: u32) -> ExperienceOrb {
|
||||
@@ -118,6 +119,7 @@ class ExperienceOrbMergeTests(unittest.TestCase):
|
||||
source = (ROOT / "crates/horizon-client/src/game.rs").read_text(encoding="utf-8")
|
||||
fixture = (FIXTURE.replace("ORB_MERGES", method(source, "orb_merges"))
|
||||
.replace("AWARD_ORB", method(source, "award_orb"))
|
||||
.replace("MERGE_NEW_ORB", method(source, "merge_new_orb"))
|
||||
.replace("MERGE_ORBS", method(source, "merge_orbs")))
|
||||
with tempfile.TemporaryDirectory() as folder:
|
||||
src, exe = Path(folder) / "orb.rs", Path(folder) / "orb.exe"
|
||||
|
||||
Reference in new issue
Block a user