mirror of
https://github.com/horizon-rust/horizon.git
synced 2026-11-07 11:06:11 -10:00
Merge pull request #2196 from kacpherek/fix/axolotl-squid-water-rules
🤖 Agent: Gate check by Ludovic's AI agent (Claude). Checked against 26.3: `Axolotl.handleAirSupply` (6000 air, refilled by water or rain, `dry_out` damage at -20) and `Squid.hurtServer`'s `spawnInk`. I pushed two fixes: `makeSound`'s voice pitch is drawn even when the squid is silent, so the ink's random draws stay in vanilla's order; and rain now uses the biome's height-adjusted temperature (`Biome.getTemperature` with the frozen modifier and the drop above sea level, `Level.isRainingAt`), not the base temperature, so an axolotl high up a mountain is not refilled by snow. On main with this head: clippy `-D warnings` clean and the 1186 horizon-client lib tests pass. CI parity runs the harnesses this head reaches. Thanks @kacpherek!
This commit is contained in:
8 files changed
+139
-3
No files matched your search
@@ -1,7 +1,7 @@
|
||||
//! Biome colours from the pinned biome definitions and colormaps
|
||||
//! (`Biome.getGrassColor`/`getFoliageColor`/..., `BlockTintSources`).
|
||||
use crate::Result;
|
||||
use horizon_core::{noise::SimplexNoise, random::LegacyRandom};
|
||||
use horizon_core::{noise::SimplexNoise, position::BlockPos, random::LegacyRandom};
|
||||
use serde_json::Value;
|
||||
use std::{collections::HashMap, fs, path::Path};
|
||||
|
||||
@@ -33,6 +33,8 @@ pub struct BiomeInfo {
|
||||
pub increased_fire_burnout: bool,
|
||||
pub name: String,
|
||||
pub temperature: f32,
|
||||
/// `temperature_modifier: frozen` (the frozen oceans).
|
||||
pub frozen: bool,
|
||||
pub downfall: f32,
|
||||
pub precipitation: bool,
|
||||
pub grass: u32,
|
||||
@@ -45,6 +47,26 @@ pub struct BiomeInfo {
|
||||
pub fog: Option<u32>,
|
||||
pub water_fog: u32,
|
||||
}
|
||||
impl BiomeInfo {
|
||||
/// `Biome.getHeightAdjustedTemperature`: the frozen modifier, then cooler
|
||||
/// with height above `sea_level + 17`.
|
||||
pub fn temperature_at(&self, pos: BlockPos, sea_level: i32) -> f32 {
|
||||
use horizon_world::worldgen::biome_temperature::{
|
||||
BiomeTemperatureSettings, TemperatureModifier,
|
||||
};
|
||||
BiomeTemperatureSettings {
|
||||
base_temperature: self.temperature,
|
||||
modifier: if self.frozen {
|
||||
TemperatureModifier::Frozen
|
||||
} else {
|
||||
TemperatureModifier::None
|
||||
},
|
||||
has_precipitation: self.precipitation,
|
||||
downfall: self.downfall,
|
||||
}
|
||||
.height_adjusted(pos, sea_level)
|
||||
}
|
||||
}
|
||||
pub struct Biomes {
|
||||
pub list: Vec<BiomeInfo>,
|
||||
pub index: HashMap<String, u16>,
|
||||
@@ -135,6 +157,7 @@ impl Biomes {
|
||||
.as_bool()
|
||||
.unwrap_or(false),
|
||||
temperature,
|
||||
frozen: v["temperature_modifier"].as_str() == Some("frozen"),
|
||||
downfall,
|
||||
precipitation: v["has_precipitation"].as_bool().unwrap_or(true),
|
||||
grass: color(&effects["grass_color"])
|
||||
|
||||
@@ -115,6 +115,7 @@ impl Game<'_> {
|
||||
"in_fire" | "campfire" | "hot_floor" => DamageKind::InFire,
|
||||
"lava" => DamageKind::Lava,
|
||||
"drown" => DamageKind::Drown,
|
||||
"dry_out" => DamageKind::DryOut,
|
||||
"magic" | "indirect_magic" | "dragon_breath" => DamageKind::Magic,
|
||||
"sting" => DamageKind::Sting,
|
||||
"cactus" | "sweet_berry_bush" => DamageKind::Cactus,
|
||||
|
||||
@@ -3524,6 +3524,7 @@ pub(super) fn damage_type(kind: horizon_client::mobs::DamageKind, player: bool)
|
||||
K::InFire => "minecraft:in_fire",
|
||||
K::Lava => "minecraft:lava",
|
||||
K::Drown => "minecraft:drown",
|
||||
K::DryOut => "minecraft:dry_out",
|
||||
K::Magic => "minecraft:magic",
|
||||
K::Sting => "minecraft:sting",
|
||||
K::Cactus => "minecraft:cactus",
|
||||
|
||||
@@ -517,6 +517,8 @@ pub enum DamageKind {
|
||||
InFire,
|
||||
Lava,
|
||||
Drown,
|
||||
/// `dry_out`: an axolotl out of water for too long.
|
||||
DryOut,
|
||||
Magic,
|
||||
Sting,
|
||||
Cactus,
|
||||
@@ -575,7 +577,11 @@ impl Damage {
|
||||
pub fn causes_panic(&self) -> bool {
|
||||
!matches!(
|
||||
self.kind,
|
||||
DamageKind::Fall | DamageKind::Drown | DamageKind::Generic | DamageKind::InWall
|
||||
DamageKind::Fall
|
||||
| DamageKind::Drown
|
||||
| DamageKind::DryOut
|
||||
| DamageKind::Generic
|
||||
| DamageKind::InWall
|
||||
)
|
||||
}
|
||||
/// DamageTypeTags.BYPASSES_ARMOR
|
||||
@@ -608,6 +614,7 @@ impl Damage {
|
||||
| DamageKind::InFire
|
||||
| DamageKind::Lava
|
||||
| DamageKind::Drown
|
||||
| DamageKind::DryOut
|
||||
| DamageKind::Magic
|
||||
| DamageKind::Cactus
|
||||
| DamageKind::Generic
|
||||
@@ -1880,6 +1887,12 @@ impl Mob {
|
||||
}
|
||||
}
|
||||
}
|
||||
// Squid.hurtServer: a squid hurt after a living entity hit it squirts ink.
|
||||
if matches!(self.species(), Species::Squid | Species::GlowSquid)
|
||||
&& self.ai.last_hurt_by.is_some()
|
||||
{
|
||||
aquatic::spawn_ink(self);
|
||||
}
|
||||
true
|
||||
}
|
||||
/// LivingEntity.knockback(power, xd, zd) towards the negative direction.
|
||||
|
||||
@@ -444,6 +444,34 @@ pub fn tick_swing(m: &mut Mob) {
|
||||
}
|
||||
m.anim.attack_anim = m.ai.swing_time as f32 / 6.0;
|
||||
}
|
||||
/// `LivingEntity.baseTick` refills a breather's air by 4, then
|
||||
/// `Axolotl.handleAirSupply` (unless the mob has no AI): water or rain fills
|
||||
/// it, anywhere else it runs down from the pre-tick value, and at -20 the
|
||||
/// axolotl dries out.
|
||||
fn axolotl_air_supply(m: &mut Mob, w: &MobWorld) {
|
||||
let before = m.air;
|
||||
m.air = (m.air + 4).min(super::axolotl::MAX_AIR);
|
||||
if m.no_ai {
|
||||
return;
|
||||
}
|
||||
// Entity.isInRain: rain at the block, or at the top of the box.
|
||||
let (x, y, z) = (
|
||||
m.position.x.floor() as i32,
|
||||
m.position.y.floor() as i32,
|
||||
m.position.z.floor() as i32,
|
||||
);
|
||||
let top = (m.position.y + f64::from(m.height())).floor() as i32;
|
||||
let in_rain = w.raining_at(x, y, z) || w.raining_at(x, top, z);
|
||||
if m.fluids.in_water || in_rain {
|
||||
m.air = super::axolotl::MAX_AIR;
|
||||
} else {
|
||||
m.air = before - 1;
|
||||
if m.air <= -20 {
|
||||
m.air = 0;
|
||||
m.damage(2.0, Damage::of(DamageKind::DryOut));
|
||||
}
|
||||
}
|
||||
}
|
||||
/// Entity.baseTick + LivingEntity.baseTick + Mob.baseTick.
|
||||
fn base_tick(m: &mut Mob, w: &MobWorld, events: &mut Vec<MobEvent>) {
|
||||
m.was_eye_in_water = m.fluids.eye_in_water;
|
||||
@@ -503,7 +531,9 @@ fn base_tick(m: &mut Mob, w: &MobWorld, events: &mut Vec<MobEvent>) {
|
||||
m.damage(1.0, Damage::of(DamageKind::InWall));
|
||||
}
|
||||
// Air supply and drowning (WaterAnimal.handleAirSupply for aquatic mobs).
|
||||
if m.kind.flags.needs_water {
|
||||
if m.species() == Species::Axolotl {
|
||||
axolotl_air_supply(m, w);
|
||||
} else if m.kind.flags.needs_water {
|
||||
if !m.fluids.in_water {
|
||||
m.air -= 1;
|
||||
if m.air == -20 {
|
||||
|
||||
@@ -24,6 +24,58 @@ const Z_BODY_ROT: usize = 3;
|
||||
const TENTACLE_SPEED: usize = 4;
|
||||
const ROTATE_SPEED: usize = 5;
|
||||
|
||||
/// `Squid.spawnInk`: the squirt sound and 30 ink particles shot from under
|
||||
/// the squid, turned by its body rotation.
|
||||
pub fn spawn_ink(m: &mut Mob) {
|
||||
let sound = if m.species() == Species::GlowSquid {
|
||||
"entity.glow_squid.squirt"
|
||||
} else {
|
||||
"entity.squid.squirt"
|
||||
};
|
||||
let kind = if m.species() == Species::GlowSquid {
|
||||
"glow_squid_ink"
|
||||
} else {
|
||||
"squid_ink"
|
||||
};
|
||||
// makeSound draws the voice pitch even for a silent squid.
|
||||
let pitch = m.voice_pitch();
|
||||
if !m.silent {
|
||||
let at = [m.position.x, m.position.y, m.position.z];
|
||||
m.outbox.push(MobEvent::Sound(
|
||||
sound,
|
||||
at,
|
||||
super::ai::sound_volume(m),
|
||||
pitch,
|
||||
));
|
||||
}
|
||||
// rotateVector: xRot(xBodyRotO), then yRot(-yBodyRotO).
|
||||
let to_radians = (std::f64::consts::PI / 180.0) as f32;
|
||||
let (x_rot, y_rot) = (
|
||||
m.anim.vo[X_BODY_ROT] * to_radians,
|
||||
-m.previous_body_yaw * to_radians,
|
||||
);
|
||||
let rotate = |v: Vec3| v.rotate_x(x_rot).rotate_y(y_rot);
|
||||
let pos = rotate(Vec3::new(0.0, -1.0, 0.0)).add_vector(m.position);
|
||||
let offset_scale: f32 = if m.is_baby() { 0.1 } else { 0.3 };
|
||||
for _ in 0..30 {
|
||||
let x = f64::from(m.random.next_float()) * 0.6 - 0.3;
|
||||
let z = f64::from(m.random.next_float()) * 0.6 - 0.3;
|
||||
let dir = rotate(Vec3::new(x, -1.0, z));
|
||||
let d = dir.scale(f64::from(offset_scale + m.random.next_float() * 2.0));
|
||||
// ServerLevel.sendParticles with a count of 0 and speed 0.1: one
|
||||
// particle moving at the float offset times the float speed.
|
||||
let speed = f64::from(0.1f32);
|
||||
m.outbox.push(MobEvent::ParticleAt {
|
||||
kind,
|
||||
at: [pos.x, pos.y + 0.5, pos.z],
|
||||
velocity: [
|
||||
speed * f64::from(d.x as f32),
|
||||
speed * f64::from(d.y as f32),
|
||||
speed * f64::from(d.z as f32),
|
||||
],
|
||||
});
|
||||
}
|
||||
}
|
||||
pub fn register(m: &mut Mob) {
|
||||
use Species as S;
|
||||
match m.species() {
|
||||
@@ -36,6 +88,7 @@ pub fn register(m: &mut Mob) {
|
||||
m.anim.v[TENTACLE_SPEED] = speed;
|
||||
}
|
||||
S::Axolotl => {
|
||||
m.air = super::axolotl::MAX_AIR;
|
||||
// Native controller scaling is essential: the base speed attribute is 1.0.
|
||||
m.ai.mov.kind = MoveKind::SmoothSwimming {
|
||||
max_turn_x: 85.0,
|
||||
|
||||
@@ -3,6 +3,8 @@ use super::{Mob, MobEvent, MobWorld, Species, Who, animal_brain, keyframes::Anim
|
||||
use horizon_core::random::RandomSource;
|
||||
pub const PLAY_DEAD_TICKS: i32 = 200;
|
||||
pub const HUNTING_COOLDOWN: i64 = 2400;
|
||||
/// `Axolotl.getMaxAirSupply`.
|
||||
pub const MAX_AIR: i32 = 6000;
|
||||
|
||||
/// AxolotlAttackablesSensor: always-hostile targets bypass the hunting cooldown.
|
||||
pub fn prey_eligible(
|
||||
|
||||
@@ -206,6 +206,19 @@ impl MobWorld<'_> {
|
||||
}
|
||||
self.world.light(x, y, z) >> 4 == 15
|
||||
}
|
||||
/// Level.isRainingAt: raining, open to the sky, nothing motion blocking
|
||||
/// above, and the biome's precipitation at the position is rain, not snow
|
||||
/// or none.
|
||||
pub fn raining_at(&self, x: i32, y: i32, z: i32) -> bool {
|
||||
if !self.raining || !self.can_see_sky(x, y, z) || self.motion_blocking_height(x, z) > y {
|
||||
return false;
|
||||
}
|
||||
let Some(chunk) = self.world.chunks.get(&(x >> 4, z >> 4)) else {
|
||||
return false;
|
||||
};
|
||||
let biome = self.biomes.get(chunk.biome(x, y, z));
|
||||
biome.precipitation && biome.temperature_at(BlockPos::new(x, y, z), self.sea_level) >= 0.15
|
||||
}
|
||||
/// Level.getLightLevelDependentMagicValue: the dimension's brightness curve
|
||||
/// at the raw (sky darkened) light level.
|
||||
pub fn brightness(&self, x: i32, y: i32, z: i32) -> f32 {
|
||||
|
||||
Reference in new issue
Block a user