mirror of
https://github.com/horizon-rust/horizon.git
synced 2026-11-08 08:07:53 -10:00
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:
1 parent
5f269772d9
commit
7e459e5fb1
7 files changed
+69
-34
No files matched your search
@@ -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,
|
||||
},
|
||||
);
|
||||
|
||||
@@ -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,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);
|
||||
|
||||
@@ -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 ---------------------------------------------------------------
|
||||
|
||||
Reference in new issue
Block a user