Frogs long jump: level and unseeded randoms, activity gate TODO

Address the review on #3544:
- goals::Cx carries the level random (the one the brains already get);
  the weighted jump-candidate pick and the three TIME_BETWEEN_LONG_JUMPS
  samples draw from it, as LongJumpToRandomPos/LongJumpMidJump do.
- The angle shuffle in calculateOptimalJumpVector uses a fresh unseeded
  random (Collections.shuffle's global one), not the frog's.
- The preferred-block roll stays on the frog's random (vanilla uses the
  body's random there).
- TODO in LongJump::can_use for FrogAi.initJumpActivity's TEMPTING_PLAYER /
  BREED_TARGET gate and LAY_SPAWN priority: frogs have no tempting,
  breeding or frogspawn here yet.

Other mobs only get the new field threaded through; nothing else reads it.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01UE3PMKbnTcxfBSeT5uQWCK
This commit is contained in:
degepieandClaude Opus 5.5 committed 2026-10-29 16:15:54 +05:00
1 parent 5f269772d9
commit 7e459e5fb1
7 files changed
+69 -34

No files matched your search

+10 -2
View File
@@ -1181,7 +1181,11 @@ fn server_ai_step(
let mut targets = std::mem::take(&mut m.ai.targets);
let mut goals = std::mem::take(&mut m.ai.goals);
{
let mut cx = Cx { w, events };
let mut cx = Cx {
w,
lr: level_random,
events,
};
if id_based % 2 != 0 && tick_count > 1 {
targets.tick_running(m, &mut cx, false);
goals.tick_running(m, &mut cx, false);
@@ -1222,7 +1226,11 @@ fn server_ai_step(
super::animal_brain::tick(m, w, level_random, events);
}
{
let mut cx = Cx { w, events };
let mut cx = Cx {
w,
lr: level_random,
events,
};
custom_server_ai_step(m, &mut cx);
}
if frozen {
@@ -2396,6 +2396,7 @@ fn axolotl_action(m: &mut Mob, cx: &mut Bx, action: AxolotlAction, until: &mut i
target,
&mut super::goals::Cx {
w: cx.w,
lr: cx.lr,
events: cx.events,
},
);
+38 -26
View File
@@ -13,7 +13,10 @@ use super::{
},
sync,
};
use horizon_core::{geometry::Vec3, random::RandomSource};
use horizon_core::{
geometry::Vec3,
random::{LegacyRandom, RandomSource},
};
const JUMP: usize = 0;
const CROAK: usize = 1;
@@ -360,10 +363,11 @@ fn set_jump_cooldown(m: &mut Mob, ticks: Option<i64>) {
(None, None) => {}
}
}
/// `TIME_BETWEEN_LONG_JUMPS.sample`, divided as the caller asks.
fn sample_cooldown(m: &mut Mob, div: i32) {
/// `TIME_BETWEEN_LONG_JUMPS.sample` from the level's random, divided as the
/// caller asks.
fn sample_cooldown(m: &mut Mob, lr: &mut LegacyRandom, div: i32) {
let (lo, hi) = TIME_BETWEEN_LONG_JUMPS;
let t = m.random.next_int_between_inclusive(lo, hi).unwrap_or(lo) / div;
let t = lr.next_int_between_inclusive(lo, hi).unwrap_or(lo) / div;
set_jump_cooldown(m, Some(i64::from(t)));
}
@@ -414,13 +418,14 @@ struct LongJump {
in_air: Option<i32>,
}
impl LongJump {
/// `WeightedRandom.getRandomItem` over the candidates, removed once drawn.
fn draw(&mut self, m: &mut Mob) -> Option<Candidate> {
/// `WeightedRandom.getRandomItem` with the level's random over the
/// candidates, removed once drawn.
fn draw(&mut self, lr: &mut LegacyRandom) -> Option<Candidate> {
let total: i32 = self.candidates.iter().map(|c| c.1).sum();
if total == 0 {
return None;
}
let mut pick = m.random.next_int_bound(total).unwrap_or(0);
let mut pick = lr.next_int_bound(total).unwrap_or(0);
let i = self.candidates.iter().position(|c| {
pick -= c.1;
pick < 0
@@ -429,12 +434,12 @@ impl LongJump {
}
/// `LongJumpToPreferredBlock.getJumpCandidate`: while it wants them, the
/// first drawn spot over a preferred block, else the first one passed over.
fn candidate(&mut self, m: &mut Mob, w: &MobWorld) -> Option<Candidate> {
fn candidate(&mut self, w: &MobWorld, lr: &mut LegacyRandom) -> Option<Candidate> {
if !self.wanting_preferred {
return self.draw(m);
return self.draw(lr);
}
while !self.candidates.is_empty() {
if let Some(c) = self.draw(m) {
if let Some(c) = self.draw(lr) {
let [x, y, z] = c.0;
if w.has_tag("minecraft:frog_prefer_jump_to", w.block_name(x, y - 1, z)) {
return Some(c);
@@ -446,9 +451,9 @@ impl LongJump {
}
/// `LongJumpToRandomPos.pickCandidate`: the first acceptable spot it can
/// reach in an arc but not by an 8 block path.
fn pick(&mut self, m: &mut Mob, w: &MobWorld) {
fn pick(&mut self, m: &mut Mob, w: &MobWorld, lr: &mut LegacyRandom) {
while !self.candidates.is_empty() {
let Some((target, _)) = self.candidate(m, w) else {
let Some((target, _)) = self.candidate(w, lr) else {
continue;
};
let here = [m.position.x.floor() as i32, m.position.z.floor() as i32];
@@ -484,7 +489,7 @@ impl LongJump {
.push(MobEvent::Sound("entity.frog.step", at, 2.0, 1.0));
}
set_pose(m, sync::STANDING);
sample_cooldown(m, 1);
sample_cooldown(m, cx.lr, 1);
}
m.extra.frog.discard_friction = false;
*self = Self::default();
@@ -506,11 +511,13 @@ fn candidates([x, y, z]: [i32; 3]) -> Vec<Candidate> {
out
}
/// `LongJumpToRandomPos.calculateOptimalJumpVector`: the angles shuffled, the
/// first arc that reaches the target clear of blocks.
fn optimal_jump_vector(m: &mut Mob, w: &MobWorld, target: Vec3) -> Option<Vec3> {
/// first arc that reaches the target clear of blocks. `Collections.shuffle`
/// without a random draws from an unseeded one, not the mob's or the level's.
fn optimal_jump_vector(m: &Mob, w: &MobWorld, target: Vec3) -> Option<Vec3> {
let mut random = crate::sound_engine::SoundEngine::unseeded();
let mut angles = ALLOWED_ANGLES;
for i in (2..=angles.len()).rev() {
let j = m.random.next_int_bound(i as i32).unwrap_or(0) as usize;
let j = random.next_int_bound(i as i32).unwrap_or(0) as usize;
angles.swap(i - 1, j);
}
let max = (m.attrs.jump_strength * f64::from(JUMP_VELOCITY_MULTIPLIER)) as f32;
@@ -530,7 +537,11 @@ impl Goal for LongJump {
true
}
fn can_use(&mut self, m: &mut Mob, cx: &mut Cx) -> bool {
// The activity: no cooldown and out of water.
// The activity: no cooldown and out of water. FrogAi.initJumpActivity
// also wants TEMPTING_PLAYER and BREED_TARGET absent, and the
// LAY_SPAWN activity (IS_PREGNANT) goes before LONG_JUMP.
// TODO: gate on them once frogs follow frog_food, breed and lay
// frogspawn here; none of the three exists for frogs yet.
if jump_cooldown(m).is_some() || m.fluids.in_water {
return false;
}
@@ -543,7 +554,7 @@ impl Goal for LongJump {
let can =
m.on_ground && !m.fluids.in_lava && cx.w.block_name(x, y, z) != "minecraft:honey_block";
if !can {
sample_cooldown(m, 2);
sample_cooldown(m, cx.lr, 2);
}
can
}
@@ -563,7 +574,7 @@ impl Goal for LongJump {
// LongJumpToPreferredBlock.start.
self.wanting_preferred = m.random.next_float() < PREFERRED_BLOCKS_CHANCE;
}
fn can_continue(&mut self, m: &mut Mob, _cx: &mut Cx) -> bool {
fn can_continue(&mut self, m: &mut Mob, cx: &mut Cx) -> bool {
if self.mid_jump {
// LongJumpMidJump starts the tick after the leap and runs until
// the frog is on the ground again, for 100 ticks at most; water
@@ -585,7 +596,7 @@ impl Goal for LongJump {
&& !m.fluids.in_water
&& !(self.chosen.is_none() && self.candidates.is_empty());
if !valid {
sample_cooldown(m, 2);
sample_cooldown(m, cx.lr, 2);
}
valid
}
@@ -598,7 +609,7 @@ impl Goal for LongJump {
}
let Some(jump) = self.chosen else {
self.tries -= 1;
self.pick(m, cx.w);
self.pick(m, cx.w, cx.lr);
return;
};
m.look_at(self.look);
@@ -656,13 +667,12 @@ mod tests {
/// list, and the drawn candidate is removed.
#[test]
fn a_draw_takes_the_weighted_candidate_out() {
let mut m = frog();
let mut j = LongJump {
candidates: vec![([0, 0, 0], 1), ([1, 0, 0], 4), ([2, 0, 0], 9)],
..LongJump::default()
};
let mut r = LegacyRandom::new(7);
m.random = r.clone();
let mut lr = r.clone();
let pick = r.next_int_bound(14).unwrap();
let want = if pick < 1 {
0
@@ -671,20 +681,22 @@ mod tests {
} else {
2
};
assert_eq!(j.draw(&mut m).unwrap().0, [want, 0, 0]);
assert_eq!(j.draw(&mut lr).unwrap().0, [want, 0, 0]);
assert_eq!(j.candidates.len(), 2);
assert!(!j.candidates.iter().any(|c| c.0 == [want, 0, 0]));
}
/// `CountDownCooldownTicks`: the memory falls by one a tick and is gone
/// the tick after it reached 0; the sample is 100..=140, halved after a
/// failed search.
/// failed search, drawn from the level's random and not the frog's.
#[test]
fn the_long_jump_cooldown_counts_down_and_is_erased() {
let mut m = frog();
sample_cooldown(&mut m, 2);
let own = m.random.state();
sample_cooldown(&mut m, &mut LegacyRandom::new(3), 2);
let t = jump_cooldown(&m).unwrap();
assert!((50..=70).contains(&t));
assert_eq!(m.random.state(), own);
set_jump_cooldown(&mut m, Some(1));
set_jump_cooldown(&mut m, Some(0));
assert_eq!(jump_cooldown(&m), Some(0));
+4 -1
View File
@@ -4,7 +4,7 @@ use super::{Mob, MobEvent, MobWorld, Seen, Species, Who, world::TargetInfo};
use horizon_core::{
geometry::{Aabb, Vec3},
jmath,
random::RandomSource,
random::{LegacyRandom, RandomSource},
};
use std::sync::Arc;
@@ -16,6 +16,9 @@ pub const TARGET: u8 = 8;
/// What goals may read and emit during a tick.
pub struct Cx<'a, 'w> {
pub w: &'a MobWorld<'w>,
/// The level's random (`level.getRandom()`), for the draws vanilla's
/// behaviours make from it rather than from the mob's own.
pub lr: &'a mut LegacyRandom,
pub events: &'a mut Vec<MobEvent>,
}
pub trait Goal: std::fmt::Debug {
@@ -783,6 +783,7 @@ impl B {
m.swing();
let mut gcx = Cx {
w,
lr: cx.lr,
events: cx.events,
};
super::ai::do_hurt_target(m, target, &mut gcx);
+8 -2
View File
@@ -454,7 +454,13 @@ pub fn do_push(m: &mut Mob, w: &MobWorld, who: Who, events: &mut Vec<MobEvent>)
}
/// `Warden.doHurtTarget`: the attack keyframes (entity event 4), the impact
/// sound, a sonic boom cooldown, then `Mob.doHurtTarget`.
pub(super) fn do_hurt_target(m: &mut Mob, target: Who, w: &MobWorld, events: &mut Vec<MobEvent>) {
pub(super) fn do_hurt_target(
m: &mut Mob,
target: Who,
w: &MobWorld,
lr: &mut LegacyRandom,
events: &mut Vec<MobEvent>,
) {
m.anim.states[ROAR].stop();
m.anim.states[ATTACK].start(m.age as f32);
events.push(MobEvent::EntityEvent {
@@ -465,7 +471,7 @@ pub(super) fn do_hurt_target(m: &mut Mob, target: Who, w: &MobWorld, events: &mu
let pitch = voice_pitch(m);
sound(m, "entity.warden.attack_impact", 10.0, pitch, events);
warden_brain::mem(m).sonic_boom_cooldown.set_ttl((), 40);
let mut cx = warden_brain::cx(w, events);
let mut cx = warden_brain::cx(w, lr, events);
super::ai::do_hurt_target(m, target, &mut cx);
}
@@ -962,7 +962,7 @@ impl B {
}
mem(m).look_target.set(Tracker::Entity(target, true));
m.swing();
warden::do_hurt_target(m, target, w, cx.events);
warden::do_hurt_target(m, target, w, cx.lr, cx.events);
mem(m)
.attack_cooling_down
.set_ttl(true, MELEE_ATTACK_COOLDOWN);
@@ -1188,8 +1188,12 @@ fn swim_condition(m: &Mob) -> bool {
m.fluids.water > m.fluid_jump_threshold() || m.fluids.in_lava
}
/// The `Cx` the mob library's attacks take.
pub(super) fn cx<'a, 'w>(w: &'a MobWorld<'w>, events: &'a mut Vec<MobEvent>) -> Cx<'a, 'w> {
Cx { w, events }
pub(super) fn cx<'a, 'w>(
w: &'a MobWorld<'w>,
lr: &'a mut LegacyRandom,
events: &'a mut Vec<MobEvent>,
) -> Cx<'a, 'w> {
Cx { w, lr, events }
}
// ---- Printing ---------------------------------------------------------------