mirror of
https://github.com/horizon-rust/horizon.git
synced 2026-11-07 11:06:11 -10:00
Compare commits
141
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
5fb64f0d62 | ||
|
|
663828ad73 | ||
|
|
be47fd0f0f | ||
|
|
3b75c78a91 | ||
|
|
71206c2bcc | ||
|
|
e2dd8489a7 | ||
|
|
02e7249bfc | ||
|
|
1bd0b885be | ||
|
|
705fe123d3 | ||
|
|
4611c75a8d | ||
|
|
7ccb4d846a | ||
|
|
1fdcfdd0c9 | ||
|
|
8144b31bfd | ||
|
|
7b695ed0c7 | ||
|
|
c484ad2640 | ||
|
|
2f2c3061cc | ||
|
|
2b3629d027 | ||
|
|
7be7ee228e | ||
|
|
2e85bd6564 | ||
|
|
98443995bd | ||
|
|
c61c103660 | ||
|
|
c8428161f0 | ||
|
|
808d03f302 | ||
|
|
f8397cbb0a | ||
|
|
ba0d418637 | ||
|
|
4bada9606f | ||
|
|
e29f2ecd72 | ||
|
|
49f87fa6eb | ||
|
|
f555b61f4c | ||
|
|
d4e0a0328a | ||
|
|
5564b7c275 | ||
|
|
f0d7bd629e | ||
|
|
e44b07c075 | ||
|
|
222e6024c3 | ||
|
|
30fc75a5df | ||
|
|
758f5b33fb | ||
|
|
a01a38fe36 | ||
|
|
660ba02e2d | ||
|
|
6c8ac2c770 | ||
|
|
fe16c59bc4 | ||
|
|
17f2ddf41c | ||
|
|
9e11fc0a87 | ||
|
|
daaf6c0b48 | ||
|
|
bb761ecaeb | ||
|
|
8b92f8f477 | ||
|
|
4f72879c54 | ||
|
|
8be1f4233f | ||
|
|
49e61f02ed | ||
|
|
8934f72ba8 | ||
|
|
c1ebf5d1b0 | ||
|
|
d53114fa82 | ||
|
|
9b5c36e232 | ||
|
|
f4271d7c16 | ||
|
|
78dba05d8f | ||
|
|
0e927e6f11 | ||
|
|
9ad735b9d7 | ||
|
|
4d897de20b | ||
|
|
6f19f06634 | ||
|
|
54ab5dcf00 | ||
|
|
5ea88533e0 | ||
|
|
594394ab30 | ||
|
|
c3ef0a06e7 | ||
|
|
531212c812 | ||
|
|
2e9827a259 | ||
|
|
6c769f38ae | ||
|
|
9c3058f756 | ||
|
|
ef0ea5daad | ||
|
|
a21cf57a11 | ||
|
|
584fa2a9b5 | ||
|
|
6c09ec2cc8 | ||
|
|
4940a24423 | ||
|
|
5435eb0f17 | ||
|
|
765cef8a4e | ||
|
|
370444eed0 | ||
|
|
79fca90886 | ||
|
|
73aef0ce8e | ||
|
|
a19cad22cb | ||
|
|
7115c708e1 | ||
|
|
82c8398c6b | ||
|
|
76bebec833 | ||
|
|
a934dac2bd | ||
|
|
87d577e566 | ||
|
|
b5794fa5b3 | ||
|
|
4e82a62489 | ||
|
|
9ada76e00a | ||
|
|
2c894783aa | ||
|
|
9266e0c7c9 | ||
|
|
a68ee8009f | ||
|
|
909f15b909 | ||
|
|
da57350af5 | ||
|
|
bf64f868d1 | ||
|
|
0d496fdc8f | ||
|
|
4cad82b4a9 | ||
|
|
8a5d7bb78d | ||
|
|
400d9e6115 | ||
|
|
04b0b9aed5 | ||
|
|
5e27fd3421 | ||
|
|
e544924504 | ||
|
|
29754b857d | ||
|
|
8144385ce6 | ||
|
|
11e1ff12d0 | ||
|
|
26aef75d7c | ||
|
|
25c53cc803 | ||
|
|
348054d26e | ||
|
|
c05dc8af22 | ||
|
|
a45d0de43e | ||
|
|
688733132d | ||
|
|
9dd6933dab | ||
|
|
87b533c5a9 | ||
|
|
7f6e47b02e | ||
|
|
7fdd57ad1e | ||
|
|
f6db8b1f56 | ||
|
|
dca96dd1df | ||
|
|
e0e484e44b | ||
|
|
b3e1542e09 | ||
|
|
57e8de0007 | ||
|
|
861e7279e0 | ||
|
|
7673d38ed7 | ||
|
|
426b3c83cf | ||
|
|
f2856845f4 | ||
|
|
1d93086e33 | ||
|
|
00e15171d5 | ||
|
|
960b8aabad | ||
|
|
3f1181bab4 | ||
|
|
1596b84299 | ||
|
|
190cf4e0c2 | ||
|
|
cb488cf515 | ||
|
|
a146754b0b | ||
|
|
6977659843 | ||
|
|
d2a8cf3bdf | ||
|
|
77fe538e09 | ||
|
|
d8b8246622 | ||
|
|
b33ae69938 | ||
|
|
a529be8915 | ||
|
|
d75ef5e4f7 | ||
|
|
414a2b82a8 | ||
|
|
9958fe0e49 | ||
|
|
94600b266f | ||
|
|
d248ab2605 | ||
|
|
0c127ec1d9 | ||
|
|
7f5e3384b3 |
No files matched your search
@@ -11,7 +11,7 @@ Feature → files → harness for the most-changed areas: [docs/architecture.md]
|
||||
- **Keep your runs away from real player data.** Any game or launcher you start gets fresh `mktemp -d`
|
||||
folders in `HORIZON_HOME` (the game's data), `HORIZON_LAUNCHER_HOME`, `XDG_DATA_HOME` and
|
||||
`XDG_CONFIG_HOME`, plus `HORIZON_LAUNCHER_NO_SELF_UPDATE=1` and `HORIZON_NO_AUDIO=1`, and, for the
|
||||
launcher, `HORIZON_LAUNCHER_TRASH` and `HORIZON_LAUNCHER_CLIPBOARD` pointing into the scratch home. Never write to
|
||||
launcher, `HORIZON_LAUNCHER_TRASH`, `HORIZON_LAUNCHER_CLIPBOARD` and `HORIZON_LAUNCHER_DESKTOP` pointing into the scratch home. Never write to
|
||||
`~/.local/share/{applications,horizon,horizon-launcher}` or `~/.local/bin`.
|
||||
- **Mark what an agent posts.** A PR body starts with a line naming whose agent opened it and with what.
|
||||
Ludovic's agents write `> 🤖 Opened by Ludovic's AI agent (Claude), not by Ludovic himself.`, and a
|
||||
|
||||
Generated
+7
@@ -1398,6 +1398,7 @@ dependencies = [
|
||||
"sha1",
|
||||
"sha2",
|
||||
"symphonia",
|
||||
"unicode-bidi",
|
||||
"ureq",
|
||||
"wgpu",
|
||||
"winit",
|
||||
@@ -3767,6 +3768,12 @@ version = "1.20.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b6f5e870be6c3b371b77fe0ee0bafb859fa4964b4404c27de1d380043c4dda20"
|
||||
|
||||
[[package]]
|
||||
name = "unicode-bidi"
|
||||
version = "0.3.18"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5c1cb5db39152898a79168971543b1cb5020dff7fe43c8dc468b0885f5e29df5"
|
||||
|
||||
[[package]]
|
||||
name = "unicode-ident"
|
||||
version = "1.0.26"
|
||||
|
||||
@@ -4,7 +4,10 @@
|
||||
//! language used by every screen.
|
||||
use std::{
|
||||
collections::{BTreeMap, HashMap},
|
||||
sync::{Arc, RwLock},
|
||||
sync::{
|
||||
Arc, RwLock,
|
||||
atomic::{AtomicBool, Ordering},
|
||||
},
|
||||
};
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||
@@ -23,11 +26,16 @@ impl LanguageInfo {
|
||||
#[derive(Default)]
|
||||
pub struct Language {
|
||||
pub code: String,
|
||||
/// `LanguageInfo.bidirectional`: text without a strong character reads
|
||||
/// right to left (`ClientLanguage.defaultRightToLeft`).
|
||||
bidirectional: bool,
|
||||
map: HashMap<String, String>,
|
||||
}
|
||||
pub const DEFAULT: &str = "en_us";
|
||||
|
||||
static CURRENT: RwLock<Option<Arc<Language>>> = RwLock::new(None);
|
||||
/// The current language's `bidirectional`, read for every drawn string.
|
||||
static BIDIRECTIONAL: AtomicBool = AtomicBool::new(false);
|
||||
|
||||
/// A source of resource files: the prepared root plus packs, highest priority last.
|
||||
pub trait ResourceSource {
|
||||
@@ -134,8 +142,12 @@ impl Language {
|
||||
{
|
||||
apply_deprecated(&mut map, &bytes);
|
||||
}
|
||||
let bidirectional = available(sources)
|
||||
.iter()
|
||||
.any(|info| info.code == code && info.bidirectional);
|
||||
Self {
|
||||
code: code.to_owned(),
|
||||
bidirectional,
|
||||
map,
|
||||
}
|
||||
}
|
||||
@@ -197,11 +209,16 @@ pub fn available(sources: &[&dyn ResourceSource]) -> Vec<LanguageInfo> {
|
||||
}
|
||||
|
||||
pub fn set(language: Language) {
|
||||
BIDIRECTIONAL.store(language.bidirectional, Ordering::Relaxed);
|
||||
*CURRENT.write().unwrap_or_else(|e| e.into_inner()) = Some(Arc::new(language));
|
||||
}
|
||||
pub fn current() -> Option<Arc<Language>> {
|
||||
CURRENT.read().unwrap_or_else(|e| e.into_inner()).clone()
|
||||
}
|
||||
/// `ClientLanguage.defaultRightToLeft` of the current language.
|
||||
pub fn bidirectional() -> bool {
|
||||
BIDIRECTIONAL.load(Ordering::Relaxed)
|
||||
}
|
||||
pub fn code() -> String {
|
||||
current().map_or_else(|| DEFAULT.to_owned(), |l| l.code.clone())
|
||||
}
|
||||
|
||||
@@ -62,6 +62,7 @@ gilrs = { version = "0.11", optional = true }
|
||||
horizon-dualsense = { path = "../horizon-dualsense", optional = true }
|
||||
# HTTPS to the Minecraft services (online-mode session join).
|
||||
ureq = { version = "2", default-features = false, features = ["tls"] }
|
||||
unicode-bidi = "0.3.18"
|
||||
|
||||
[target.'cfg(unix)'.dependencies]
|
||||
# The server thread's CPU time per tick (`HORIZON_SPIKES`, spans.rs).
|
||||
|
||||
@@ -1056,6 +1056,8 @@ pub fn main() -> horizon_client::Result<()> {
|
||||
last_hurt: None,
|
||||
velocity: Vec3::ZERO,
|
||||
yaw: 0.0,
|
||||
sprinting: false,
|
||||
riding: false,
|
||||
hero: false,
|
||||
effects: Vec::new(),
|
||||
uuid: PLAYER_UUID,
|
||||
@@ -1081,6 +1083,7 @@ pub fn main() -> horizon_client::Result<()> {
|
||||
world: &world,
|
||||
registry,
|
||||
facts: &facts,
|
||||
fluid_tables: &motion.fluids,
|
||||
movement: &motion.blocks,
|
||||
collision: &collision,
|
||||
data: &data,
|
||||
|
||||
@@ -36,8 +36,9 @@ enum Animate {
|
||||
spawn_particles: bool,
|
||||
},
|
||||
/// Leaves: optional falling-leaf particle and chance; a tinted leaf takes
|
||||
/// the biome foliage colour unless a fixed colour is given (azalea).
|
||||
Leaves(Option<(T, f32)>, Option<u32>),
|
||||
/// the biome foliage colour unless a fixed colour is given (azalea); and
|
||||
/// whether they have the poplar ambient sound (`AmbientLeavesBlockSoundPlayer`).
|
||||
Leaves(Option<(T, f32)>, Option<u32>, bool),
|
||||
NetherPortal,
|
||||
EnchantingTable,
|
||||
EndRod([i32; 3]),
|
||||
@@ -48,8 +49,9 @@ enum Animate {
|
||||
CandleCake,
|
||||
BrewingStand,
|
||||
EnderChest,
|
||||
/// FallingBlock with its dust colour.
|
||||
Falling(u32),
|
||||
/// FallingBlock with its dust colour; sand also has the ambient desert sound
|
||||
/// (`SandBlock.animateTick`).
|
||||
Falling(u32, bool),
|
||||
/// A beehive or bee nest full of honey (`BeehiveBlock.animateTick`).
|
||||
Beehive,
|
||||
RedstoneOre,
|
||||
@@ -73,6 +75,8 @@ enum Animate {
|
||||
waterlogged: bool,
|
||||
},
|
||||
EndPortal,
|
||||
/// `WetSpongeBlock.animateTick`.
|
||||
WetSponge,
|
||||
}
|
||||
fn step(facing: &str) -> [i32; 3] {
|
||||
match facing {
|
||||
@@ -107,17 +111,17 @@ fn classify(name: &str, prop: &dyn Fn(&str) -> Option<String>) -> Animate {
|
||||
spawn_particles: false,
|
||||
},
|
||||
"oak_leaves" | "birch_leaves" | "jungle_leaves" | "acacia_leaves" | "dark_oak_leaves"
|
||||
| "mangrove_leaves" => Animate::Leaves(Some((T::TintedLeaves, 0.01)), None),
|
||||
"cherry_leaves" => Animate::Leaves(Some((T::CherryLeaves, 0.1)), None),
|
||||
"pale_oak_leaves" => Animate::Leaves(Some((T::PaleOakLeaves, 0.02)), None),
|
||||
"red_poplar_leaves" => Animate::Leaves(Some((T::RedPoplarLeaves, 0.01)), None),
|
||||
"orange_poplar_leaves" => Animate::Leaves(Some((T::OrangePoplarLeaves, 0.01)), None),
|
||||
"yellow_poplar_leaves" => Animate::Leaves(Some((T::YellowPoplarLeaves, 0.01)), None),
|
||||
| "mangrove_leaves" => Animate::Leaves(Some((T::TintedLeaves, 0.01)), None, false),
|
||||
"cherry_leaves" => Animate::Leaves(Some((T::CherryLeaves, 0.1)), None, false),
|
||||
"pale_oak_leaves" => Animate::Leaves(Some((T::PaleOakLeaves, 0.02)), None, false),
|
||||
"red_poplar_leaves" => Animate::Leaves(Some((T::RedPoplarLeaves, 0.01)), None, true),
|
||||
"orange_poplar_leaves" => Animate::Leaves(Some((T::OrangePoplarLeaves, 0.01)), None, true),
|
||||
"yellow_poplar_leaves" => Animate::Leaves(Some((T::YellowPoplarLeaves, 0.01)), None, true),
|
||||
// The azalea blocks drop tinted leaves of the fixed colour -9399763.
|
||||
"azalea_leaves" | "flowering_azalea_leaves" => {
|
||||
Animate::Leaves(Some((T::TintedLeaves, 0.01)), Some(0x0070_922D))
|
||||
Animate::Leaves(Some((T::TintedLeaves, 0.01)), Some(0x0070_922D), false)
|
||||
}
|
||||
"spruce_leaves" => Animate::Leaves(None, None),
|
||||
"spruce_leaves" => Animate::Leaves(None, None, false),
|
||||
"nether_portal" => Animate::NetherPortal,
|
||||
"enchanting_table" => Animate::EnchantingTable,
|
||||
"end_rod" => Animate::EndRod(facing()),
|
||||
@@ -126,16 +130,17 @@ fn classify(name: &str, prop: &dyn Fn(&str) -> Option<String>) -> Animate {
|
||||
"mycelium" => Animate::Mycelium,
|
||||
"brewing_stand" => Animate::BrewingStand,
|
||||
"ender_chest" => Animate::EnderChest,
|
||||
"sand" => Animate::Falling(0xDBD3A0),
|
||||
"red_sand" => Animate::Falling(0xA95821),
|
||||
"gravel" => Animate::Falling(0x807C7B),
|
||||
"sand" => Animate::Falling(0xDBD3A0, true),
|
||||
"red_sand" => Animate::Falling(0xA95821, true),
|
||||
"gravel" => Animate::Falling(0x807C7B, false),
|
||||
// AnvilBlock/ConcretePowderBlock.getDustColor is the map colour;
|
||||
// DragonEggBlock's is black.
|
||||
"anvil" | "chipped_anvil" | "damaged_anvil" => Animate::Falling(0xA7A7A7),
|
||||
"dragon_egg" => Animate::Falling(0x000000),
|
||||
_ if name.ends_with("_concrete_powder") => Animate::Falling(concrete_powder_color(
|
||||
name.trim_end_matches("_concrete_powder"),
|
||||
)),
|
||||
"anvil" | "chipped_anvil" | "damaged_anvil" => Animate::Falling(0xA7A7A7, false),
|
||||
"dragon_egg" => Animate::Falling(0x000000, false),
|
||||
_ if name.ends_with("_concrete_powder") => Animate::Falling(
|
||||
concrete_powder_color(name.trim_end_matches("_concrete_powder")),
|
||||
false,
|
||||
),
|
||||
"beehive" | "bee_nest" if prop("honey_level").as_deref() == Some("5") => Animate::Beehive,
|
||||
"redstone_ore" | "deepslate_redstone_ore" if lit => Animate::RedstoneOre,
|
||||
"redstone_wire" => {
|
||||
@@ -167,6 +172,10 @@ fn classify(name: &str, prop: &dyn Fn(&str) -> Option<String>) -> Animate {
|
||||
waterlogged: prop("waterlogged").as_deref() == Some("true"),
|
||||
},
|
||||
"end_portal" => Animate::EndPortal,
|
||||
"wet_sponge" => Animate::WetSponge,
|
||||
// BrushableBlock has no dust colour of its own, so the dust is the map colour.
|
||||
"suspicious_sand" => Animate::Falling(0xF7E9A3, false),
|
||||
"suspicious_gravel" => Animate::Falling(0x707070, false),
|
||||
"sculk_sensor" | "calibrated_sculk_sensor"
|
||||
if prop("sculk_sensor_phase").as_deref() == Some("active") =>
|
||||
{
|
||||
@@ -233,6 +242,10 @@ pub struct AnimateTables {
|
||||
power_transmitter: Vec<bool>,
|
||||
/// `#triggers_ambient_dried_ghast_block_sounds` (soul sand, soul soil).
|
||||
dried_ghast_sounds: Vec<bool>,
|
||||
/// `#triggers_ambient_desert_sand_block_sounds` (sand, red sand).
|
||||
desert_sand_sounds: Vec<bool>,
|
||||
/// `#required_for_poplar_leaf_ambience` (overworld natural logs).
|
||||
poplar_leaf_logs: Vec<bool>,
|
||||
/// `FireBlock.canBurn`: states with ignite odds above zero (none waterlogged).
|
||||
burnable: Vec<bool>,
|
||||
/// Biome id -> ambient particles and probabilities (`minecraft:visual/ambient_particles`).
|
||||
@@ -308,6 +321,8 @@ impl AnimateTables {
|
||||
power_provider: by_state("minecraft:enchantment_power_provider"),
|
||||
power_transmitter: by_state("minecraft:enchantment_power_transmitter"),
|
||||
dried_ghast_sounds: by_state("minecraft:triggers_ambient_dried_ghast_block_sounds"),
|
||||
desert_sand_sounds: by_state("minecraft:triggers_ambient_desert_sand_block_sounds"),
|
||||
poplar_leaf_logs: by_state("minecraft:required_for_poplar_leaf_ambience"),
|
||||
burnable,
|
||||
ambient,
|
||||
})
|
||||
@@ -396,6 +411,10 @@ impl AnimateContext<'_> {
|
||||
fn add(engine: &mut ParticleEngine, ty: T, p: [f64; 3], v: [f64; 3]) {
|
||||
engine.add(&ParticleOptions::simple(ty), p, v);
|
||||
}
|
||||
/// `Level.addAlwaysVisibleParticle`: still shown under the Minimal option now and then.
|
||||
fn add_visible(engine: &mut ParticleEngine, ty: T, p: [f64; 3], v: [f64; 3]) {
|
||||
engine.add_with(&ParticleOptions::simple(ty), p, v, false, true);
|
||||
}
|
||||
fn sound(engine: &mut ParticleEngine, name: &'static str, p: [f64; 3], volume: f32, pitch: f32) {
|
||||
engine.sounds.push((name, p, volume, pitch));
|
||||
}
|
||||
@@ -771,7 +790,7 @@ fn block_animate_tick(
|
||||
}
|
||||
}
|
||||
}
|
||||
Animate::Leaves(falling, fixed_tint) => {
|
||||
Animate::Leaves(falling, fixed_tint, poplar) => {
|
||||
let below = [pos[0], pos[1] - 1, pos[2]];
|
||||
// LeavesBlock.makeDrippingWaterParticles.
|
||||
if (ctx.raining_at)([pos[0], pos[1] + 1, pos[2]]) && next_int(engine.random(), 15) == 1
|
||||
@@ -783,6 +802,9 @@ fn block_animate_tick(
|
||||
add(engine, T::DrippingWater, p, [0.0; 3]);
|
||||
}
|
||||
}
|
||||
if poplar {
|
||||
leaves_ambient_sound(tables, engine, ctx, state, pos);
|
||||
}
|
||||
// FallingParticlesLeavesBlock.makeFallingLeavesParticles.
|
||||
if let Some((ty, chance)) = falling
|
||||
&& engine.random().next_float() < chance
|
||||
@@ -955,7 +977,7 @@ fn block_animate_tick(
|
||||
fy + 1.0 + f64::from(r.next_float()),
|
||||
fz + f64::from(r.next_float()),
|
||||
];
|
||||
add(engine, T::SulfurBubbles, p, [0.0; 3]);
|
||||
add_visible(engine, T::SulfurBubbles, p, [0.0; 3]);
|
||||
}
|
||||
if next_int(engine.random(), 10) == 0 {
|
||||
let name = "minecraft:block.potent_sulfur.noxious_gas";
|
||||
@@ -963,6 +985,36 @@ fn block_animate_tick(
|
||||
}
|
||||
}
|
||||
}
|
||||
Animate::WetSponge => {
|
||||
// Direction.getRandom: DOWN, UP, NORTH, SOUTH, WEST, EAST.
|
||||
const DIRECTIONS: [[i32; 3]; 6] = [
|
||||
[0, -1, 0],
|
||||
[0, 1, 0],
|
||||
[0, 0, -1],
|
||||
[0, 0, 1],
|
||||
[-1, 0, 0],
|
||||
[1, 0, 0],
|
||||
];
|
||||
let d = DIRECTIONS[next_int(engine.random(), 6) as usize];
|
||||
// The sponge occludes, so only a sturdy neighbour face hides the drip.
|
||||
if d != [0, 1, 0] && !ctx.sturdy([pos[0] + d[0], pos[1] + d[1], pos[2] + d[2]]) {
|
||||
let r = engine.random();
|
||||
let p = match d {
|
||||
[0, -1, 0] => [fx + r.next_double(), fy - 0.05, fz + r.next_double()],
|
||||
[dx, _, 0] => {
|
||||
let y = fy + r.next_double() * 0.8;
|
||||
let z = fz + r.next_double();
|
||||
[fx + if dx > 0 { 1.1 } else { 0.05 }, y, z]
|
||||
}
|
||||
[_, _, dz] => {
|
||||
let y = fy + r.next_double() * 0.8;
|
||||
let x = fx + r.next_double();
|
||||
[x, y, fz + if dz > 0 { 1.1 } else { 0.05 }]
|
||||
}
|
||||
};
|
||||
add(engine, T::DrippingWater, p, [0.0; 3]);
|
||||
}
|
||||
}
|
||||
Animate::Mycelium => {
|
||||
let r = engine.random();
|
||||
if next_int(r, 10) == 0 {
|
||||
@@ -1045,7 +1097,7 @@ fn block_animate_tick(
|
||||
add(engine, T::Portal, p, v);
|
||||
}
|
||||
}
|
||||
Animate::Falling(color) => {
|
||||
Animate::Falling(color, sand) => {
|
||||
if next_int(engine.random(), 16) == 0 {
|
||||
let below = ctx.facts.get(ctx.state([pos[0], pos[1] - 1, pos[2]]));
|
||||
// FallingBlock.isFree: air, fire, liquids or replaceable.
|
||||
@@ -1062,6 +1114,14 @@ fn block_animate_tick(
|
||||
);
|
||||
}
|
||||
}
|
||||
// AmbientDesertBlockSoundsPlayer.playAmbientSandSounds: only for the air block.
|
||||
if sand
|
||||
&& ctx.state([pos[0], pos[1] + 1, pos[2]]) == ctx.world.air
|
||||
&& next_int(engine.random(), 2100) == 0
|
||||
&& sand_sound_nearby(tables, ctx, pos)
|
||||
{
|
||||
sound(engine, "minecraft:block.sand.idle", [fx, fy, fz], 1.0, 1.0);
|
||||
}
|
||||
}
|
||||
Animate::Beehive => {
|
||||
// BeehiveBlock.animateTick: nextInt(1) + 1 attempts.
|
||||
@@ -1179,7 +1239,7 @@ fn block_animate_tick(
|
||||
}
|
||||
Animate::BubbleColumn { drag } => {
|
||||
if drag {
|
||||
add(engine, T::CurrentDown, [fx + 0.5, fy + 0.8, fz], [0.0; 3]);
|
||||
add_visible(engine, T::CurrentDown, [fx + 0.5, fy + 0.8, fz], [0.0; 3]);
|
||||
let r = engine.random();
|
||||
if next_int(r, 200) == 0 {
|
||||
let (volume, pitch) = (0.2 + r.next_float() * 0.2, 0.9 + r.next_float() * 0.15);
|
||||
@@ -1192,7 +1252,7 @@ fn block_animate_tick(
|
||||
);
|
||||
}
|
||||
} else {
|
||||
add(
|
||||
add_visible(
|
||||
engine,
|
||||
T::BubbleColumnUp,
|
||||
[fx + 0.5, fy, fz + 0.5],
|
||||
@@ -1204,7 +1264,7 @@ fn block_animate_tick(
|
||||
fy + f64::from(r.next_float()),
|
||||
fz + f64::from(r.next_float()),
|
||||
];
|
||||
add(engine, T::BubbleColumnUp, p, [0.0, 0.04, 0.0]);
|
||||
add_visible(engine, T::BubbleColumnUp, p, [0.0, 0.04, 0.0]);
|
||||
let r = engine.random();
|
||||
if next_int(r, 200) == 0 {
|
||||
let (volume, pitch) = (0.2 + r.next_float() * 0.2, 0.9 + r.next_float() * 0.15);
|
||||
@@ -1282,6 +1342,91 @@ fn block_animate_tick(
|
||||
}
|
||||
}
|
||||
}
|
||||
/// `AmbientLeavesBlockSoundPlayer.playAmbientLeavesSounds` for the poplar leaves:
|
||||
/// one log and three leaves of the same kind around the block.
|
||||
fn leaves_ambient_sound(
|
||||
tables: &AnimateTables,
|
||||
engine: &mut ParticleEngine,
|
||||
ctx: &AnimateContext,
|
||||
state: BlockStateId,
|
||||
pos: [i32; 3],
|
||||
) {
|
||||
if next_int(engine.random(), 300) != 0 {
|
||||
return;
|
||||
}
|
||||
let Some(name) = ctx.registry.state(state).map(|s| &s.name) else {
|
||||
return;
|
||||
};
|
||||
let (mut logs, mut leaves) = (0, 0);
|
||||
for d in [[0, -1], [1, 0], [0, 1], [-1, 0]] {
|
||||
let neighbor = ctx.state([pos[0] + d[0], pos[1], pos[2] + d[1]]);
|
||||
if tables
|
||||
.poplar_leaf_logs
|
||||
.get(neighbor.0 as usize)
|
||||
.copied()
|
||||
.unwrap_or(false)
|
||||
{
|
||||
logs += 1;
|
||||
}
|
||||
if ctx
|
||||
.registry
|
||||
.state(neighbor)
|
||||
.is_some_and(|s| &s.name == name)
|
||||
{
|
||||
leaves += 1;
|
||||
}
|
||||
if logs == 1 && leaves == 3 {
|
||||
let p = pos.map(f64::from);
|
||||
sound(engine, "minecraft:block.poplar_leaves.ambient", p, 1.0, 1.0);
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
/// `AmbientDesertBlockSoundsPlayer.shouldPlayAmbientSandSound`: sand in the
|
||||
/// surface column eight blocks away on all four sides.
|
||||
fn sand_sound_nearby(tables: &AnimateTables, ctx: &AnimateContext, pos: [i32; 3]) -> bool {
|
||||
let is_sand = |p: [i32; 3]| {
|
||||
tables
|
||||
.desert_sand_sounds
|
||||
.get(ctx.state(p).0 as usize)
|
||||
.copied()
|
||||
.unwrap_or(false)
|
||||
};
|
||||
let (mut found, mut checked) = (0, 0);
|
||||
for d in [[0, -8], [8, 0], [0, 8], [-8, 0]] {
|
||||
let (x, z) = (pos[0] + d[0], pos[2] + d[1]);
|
||||
// columnContainsTriggeringBlock.
|
||||
let surface = ctx.world.world_surface(x, z, |s| ctx.facts.is_air(s)) - 1;
|
||||
let matches = if (surface - pos[1]).abs() > 5 {
|
||||
let mut y = pos[1] + 6;
|
||||
let mut above_air = ctx.is_air([x, y, z]);
|
||||
y -= 1;
|
||||
let mut hit = false;
|
||||
for _ in 0..10 {
|
||||
if above_air && is_sand([x, y, z]) {
|
||||
hit = true;
|
||||
break;
|
||||
}
|
||||
above_air = ctx.is_air([x, y, z]);
|
||||
y -= 1;
|
||||
}
|
||||
hit
|
||||
} else {
|
||||
ctx.is_air([x, surface + 1, z]) && is_sand([x, surface, z])
|
||||
};
|
||||
if matches {
|
||||
found += 1;
|
||||
if found > 3 {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
checked += 1;
|
||||
if 4 - checked + found < 3 {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
false
|
||||
}
|
||||
/// `AbstractCandleBlock.addParticlesAndSound`.
|
||||
fn candle(engine: &mut ParticleEngine, p: [f64; 3]) {
|
||||
let r = engine.random();
|
||||
|
||||
@@ -719,11 +719,6 @@ impl Audio {
|
||||
self.play(event, at, volume, pitch);
|
||||
self.capture = capture;
|
||||
}
|
||||
/// `LivingEntity.handleDamageEvent`'s pitch, `(r - r) * 0.2 + 1`, drawn on the
|
||||
/// client (the entity's own random there, never the server's world random).
|
||||
pub fn hurt_pitch(&mut self) -> f32 {
|
||||
(self.random.next_float() - self.random.next_float()) * 0.2 + 1.0
|
||||
}
|
||||
/// `AbstractWidget.playButtonClickSound`: an unseeded, relative UI sound.
|
||||
pub fn play_button_click(&mut self) {
|
||||
self.play_instance(
|
||||
|
||||
@@ -0,0 +1,302 @@
|
||||
//! Right-to-left text (`FormattedBidiReorder.reorder`, behind
|
||||
//! `Language.getVisualOrder`): the logical text a player types becomes the
|
||||
//! glyphs a font draws left to right. Arabic letters are shaped into their
|
||||
//! joined presentation forms (ICU's `ArabicShaping` with `LETTERS_SHAPE`), the
|
||||
//! text is reordered by the Unicode bidirectional algorithm, and characters in
|
||||
//! right-to-left runs are mirrored. Left-to-right text comes back unchanged.
|
||||
use crate::text::{Span, Text};
|
||||
use std::borrow::Cow;
|
||||
use unicode_bidi::{
|
||||
BidiClass, Direction, Level, ParagraphBidiInfo, bidi_class, get_base_direction,
|
||||
};
|
||||
|
||||
/// How a letter joins its neighbours, as ICU's shaper sees it: `Right` joins
|
||||
/// the letter before it only, `Dual` both neighbours, `Mark` is skipped over
|
||||
/// (diacritics) and `None` never joins.
|
||||
#[derive(Clone, Copy, PartialEq)]
|
||||
enum Join {
|
||||
None,
|
||||
Right,
|
||||
Dual,
|
||||
Mark,
|
||||
}
|
||||
use Join::{Dual, Mark, Right};
|
||||
|
||||
/// ICU's joining table (`araLink`): code point ranges and their class, with the
|
||||
/// lam-alef ligatures at the end. Anything else is `Join::None`.
|
||||
const JOINS: &[(u32, u32, Join)] = &[
|
||||
(0x0622, 0x0625, Right),
|
||||
(0x0626, 0x0626, Dual),
|
||||
(0x0627, 0x0627, Right),
|
||||
(0x0628, 0x0628, Dual),
|
||||
(0x0629, 0x0629, Right),
|
||||
(0x062A, 0x062E, Dual),
|
||||
(0x062F, 0x0632, Right),
|
||||
(0x0633, 0x063A, Dual),
|
||||
(0x0640, 0x0647, Dual),
|
||||
(0x0648, 0x0649, Right),
|
||||
(0x064A, 0x064A, Dual),
|
||||
(0x064B, 0x0656, Mark),
|
||||
(0x0670, 0x0670, Mark),
|
||||
(0x0671, 0x0673, Right),
|
||||
(0x0675, 0x0677, Right),
|
||||
(0x0678, 0x0687, Dual),
|
||||
(0x0688, 0x0699, Right),
|
||||
(0x069A, 0x06B9, Dual),
|
||||
(0x06BA, 0x06BA, Right),
|
||||
(0x06BB, 0x06BF, Dual),
|
||||
(0x06C0, 0x06C0, Right),
|
||||
(0x06C1, 0x06C1, Dual),
|
||||
(0x06C2, 0x06CB, Right),
|
||||
(0x06CC, 0x06CC, Dual),
|
||||
(0x06CD, 0x06CD, Right),
|
||||
(0x06CE, 0x06D1, Dual),
|
||||
(0x06D2, 0x06D3, Right),
|
||||
(0x200D, 0x200D, Dual),
|
||||
(0x206D, 0x206F, Mark),
|
||||
(0xFEF5, 0xFEFC, Right),
|
||||
];
|
||||
|
||||
/// Letters that have joined forms and the code point of the isolated one; the
|
||||
/// final, initial and medial forms follow it (a right-joining letter has the
|
||||
/// isolated and final only). The lam-alef ligatures are their own letters.
|
||||
/// ICU leaves the extended letters (پ چ ژ ک گ ی and the rest of U+0679 to
|
||||
/// U+06D3) unchanged, so they are in `JOINS` but not here.
|
||||
#[rustfmt::skip]
|
||||
const FORMS: &[(u32, u32)] = &[
|
||||
(0x0622, 0xFE81), (0x0623, 0xFE83), (0x0624, 0xFE85), (0x0625, 0xFE87), (0x0626, 0xFE89),
|
||||
(0x0627, 0xFE8D), (0x0628, 0xFE8F), (0x0629, 0xFE93), (0x062A, 0xFE95), (0x062B, 0xFE99),
|
||||
(0x062C, 0xFE9D), (0x062D, 0xFEA1), (0x062E, 0xFEA5), (0x062F, 0xFEA9), (0x0630, 0xFEAB),
|
||||
(0x0631, 0xFEAD), (0x0632, 0xFEAF), (0x0633, 0xFEB1), (0x0634, 0xFEB5), (0x0635, 0xFEB9),
|
||||
(0x0636, 0xFEBD), (0x0637, 0xFEC1), (0x0638, 0xFEC5), (0x0639, 0xFEC9), (0x063A, 0xFECD),
|
||||
(0x0641, 0xFED1), (0x0642, 0xFED5), (0x0643, 0xFED9), (0x0644, 0xFEDD), (0x0645, 0xFEE1),
|
||||
(0x0646, 0xFEE5), (0x0647, 0xFEE9), (0x0648, 0xFEED), (0x0649, 0xFEEF), (0x064A, 0xFEF1),
|
||||
(0xFEF5, 0xFEF5), (0xFEF7, 0xFEF7), (0xFEF9, 0xFEF9), (0xFEFB, 0xFEFB),
|
||||
];
|
||||
|
||||
/// `Bidi_Mirroring_Glyph` (`UCharacter.getMirror`): each pair is two
|
||||
/// neighbouring characters that mirror each other.
|
||||
const MIRRORED: &str = "\
|
||||
()<>[]{}«»༺༻༼༽᚛᚜‹›⁅⁆⁽⁾₍₎∈∋∉∌∊∍∕⧵∟⯾∠⦣∡⦛∢⦠∤⫮∼∽≃⋍≅≌≒≓\
|
||||
≔≕≤≥≦≧≨≩≪≫≮≯≰≱≲≳≴≵≶≷≸≹≺≻≼≽≾≿⊀⊁⊂⊃⊄⊅⊆⊇⊈⊉⊊⊋⊏⊐⊑⊒⊘⦸⊢⊣⊦⫞\
|
||||
⊨⫤⊩⫣⊫⫥⊰⊱⊲⊳⊴⊵⊶⊷⊸⟜⋉⋊⋋⋌⋐⋑⋖⋗⋘⋙⋚⋛⋜⋝⋞⋟⋠⋡⋢⋣⋤⋥⋦⋧⋨⋩⋪⋫⋬⋭⋰⋱⋲⋺\
|
||||
⋳⋻⋴⋼⋶⋽⋷⋾⌈⌉⌊⌋〈〉❨❩❪❫❬❭❮❯❰❱❲❳❴❵⟃⟄⟅⟆⟈⟉⟋⟍⟕⟖⟝⟞⟢⟣⟤⟥⟦⟧⟨⟩⟪⟫\
|
||||
⟬⟭⟮⟯⦃⦄⦅⦆⦇⦈⦉⦊⦋⦌⦍⦐⦎⦏⦑⦒⦓⦔⦕⦖⦗⦘⦤⦥⦨⦩⦪⦫⦬⦭⦮⦯⧀⧁⧄⧅⧏⧐⧑⧒⧔⧕⧘⧙⧚⧛\
|
||||
⧨⧩⧸⧹⧼⧽⨫⨬⨭⨮⨴⨵⨼⨽⩤⩥⩹⩺⩻⩼⩽⩾⩿⪀⪁⪂⪃⪄⪅⪆⪇⪈⪉⪊⪋⪌⪍⪎⪏⪐⪑⪒⪓⪔⪕⪖⪗⪘⪙⪚\
|
||||
⪛⪜⪝⪞⪟⪠⪡⪢⪦⪧⪨⪩⪪⪫⪬⪭⪯⪰⪱⪲⪳⪴⪵⪶⪷⪸⪹⪺⪻⪼⪽⪾⪿⫀⫁⫂⫃⫄⫅⫆⫇⫈⫉⫊⫋⫌⫍⫎⫏⫐\
|
||||
⫑⫒⫓⫔⫕⫖⫬⫭⫷⫸⫹⫺⸂⸃⸄⸅⸉⸊⸌⸍⸜⸝⸠⸡⸢⸣⸤⸥⸦⸧⸨⸩⹕⹖⹗⹘⹙⹚⹛⹜〈〉《》「」『』【】\
|
||||
〔〕〖〗〘〙〚〛﹙﹚﹛﹜﹝﹞﹤﹥()<>[]{}⦅⦆「」";
|
||||
|
||||
fn join(c: char) -> Join {
|
||||
let c = c as u32;
|
||||
JOINS
|
||||
.iter()
|
||||
.find(|&&(from, to, _)| (from..=to).contains(&c))
|
||||
.map_or(Join::None, |&(_, _, join)| join)
|
||||
}
|
||||
|
||||
/// The letter whose joined form `c` is (ICU shapes the forms again), or `c`.
|
||||
fn letter_of(c: char) -> char {
|
||||
let c32 = c as u32;
|
||||
if !matches!(c32, 0xFE70..=0xFEFC) {
|
||||
return c;
|
||||
}
|
||||
if (0xFE70..0xFE80).contains(&c32) {
|
||||
// The diacritics' forms pair up from fathatan on.
|
||||
return char::from_u32(0x064B + (c32 - 0xFE70) / 2).unwrap_or(c);
|
||||
}
|
||||
FORMS
|
||||
.iter()
|
||||
.find(|&&(letter, iso)| {
|
||||
let forms = if join(char::from_u32(letter).unwrap_or(c)) == Dual {
|
||||
4
|
||||
} else {
|
||||
2
|
||||
};
|
||||
(iso..iso + forms).contains(&c32)
|
||||
})
|
||||
.and_then(|&(letter, _)| char::from_u32(letter))
|
||||
.unwrap_or(c)
|
||||
}
|
||||
|
||||
/// `ArabicShaping(LETTERS_SHAPE).shape`: lam followed by an alef (diacritics
|
||||
/// may sit between) becomes one ligature, then every letter takes the isolated,
|
||||
/// final, initial or medial form its neighbours call for. Diacritics take the
|
||||
/// tatweel form between two joined letters. The text can only get shorter.
|
||||
fn shape(text: &[char]) -> Vec<char> {
|
||||
let letters: Vec<char> = text.iter().map(|&c| letter_of(c)).collect();
|
||||
let mut chars = Vec::with_capacity(letters.len());
|
||||
let mut i = 0;
|
||||
while i < letters.len() {
|
||||
if letters[i] == '\u{644}' {
|
||||
let mut at = i + 1;
|
||||
while letters.get(at).is_some_and(|&c| join(c) == Mark) {
|
||||
at += 1;
|
||||
}
|
||||
let alef = letters.get(at).and_then(|&c| {
|
||||
['\u{622}', '\u{623}', '\u{625}', '\u{627}']
|
||||
.iter()
|
||||
.position(|&a| a == c)
|
||||
});
|
||||
if let Some(alef) = alef {
|
||||
chars.push(char::from_u32(0xFEF5 + 2 * alef as u32).unwrap_or('\u{644}'));
|
||||
chars.extend_from_slice(&letters[i + 1..at]);
|
||||
i = at + 1;
|
||||
continue;
|
||||
}
|
||||
}
|
||||
chars.push(letters[i]);
|
||||
i += 1;
|
||||
}
|
||||
// Whether the nearest letter before joins the next one, and the joins of
|
||||
// the nearest letter after.
|
||||
let mut joins_next = false;
|
||||
let mut out = Vec::with_capacity(chars.len());
|
||||
for (i, &c) in chars.iter().enumerate() {
|
||||
let next_joins = chars[i + 1..]
|
||||
.iter()
|
||||
.map(|&n| join(n))
|
||||
.find(|&j| j != Mark)
|
||||
.is_some_and(|j| j != Join::None);
|
||||
let kind = join(c);
|
||||
let shaped = match kind {
|
||||
Join::None => {
|
||||
joins_next = false;
|
||||
c
|
||||
}
|
||||
Mark => match c as u32 {
|
||||
m @ 0x064B..=0x0652 => {
|
||||
let iso = 0xFE70 + 2 * (m - 0x064B);
|
||||
let tatweel = joins_next && next_joins && !matches!(m, 0x064C | 0x064D);
|
||||
char::from_u32(iso + u32::from(tatweel)).unwrap_or(c)
|
||||
}
|
||||
_ => c,
|
||||
},
|
||||
Right | Dual => {
|
||||
let to_next = kind == Dual && next_joins;
|
||||
let form = match (joins_next, to_next) {
|
||||
(false, false) => 0,
|
||||
(true, false) => 1,
|
||||
(false, true) => 2,
|
||||
(true, true) => 3,
|
||||
};
|
||||
joins_next = kind == Dual;
|
||||
FORMS
|
||||
.binary_search_by_key(&(c as u32), |&(letter, _)| letter)
|
||||
.ok()
|
||||
.and_then(|at| char::from_u32(FORMS[at].1 + form))
|
||||
.unwrap_or(c)
|
||||
}
|
||||
};
|
||||
out.push(shaped);
|
||||
}
|
||||
out
|
||||
}
|
||||
|
||||
/// `UCharacter.getMirror`.
|
||||
fn mirror(c: char) -> char {
|
||||
if bidi_class(c) != BidiClass::ON {
|
||||
return c;
|
||||
}
|
||||
let mut pairs = MIRRORED.chars();
|
||||
while let (Some(a), Some(b)) = (pairs.next(), pairs.next()) {
|
||||
if c == a {
|
||||
return b;
|
||||
}
|
||||
if c == b {
|
||||
return a;
|
||||
}
|
||||
}
|
||||
c
|
||||
}
|
||||
|
||||
/// Whether a character can change anything: right-to-left letters and numbers,
|
||||
/// the explicit direction controls and the diacritics that shaping turns into
|
||||
/// letters. Without any, left-to-right text keeps its order.
|
||||
fn directional(c: char) -> bool {
|
||||
c as u32 >= 0x0590
|
||||
&& (matches!(c as u32, 0x064B..=0x0652)
|
||||
|| matches!(
|
||||
bidi_class(c),
|
||||
BidiClass::R
|
||||
| BidiClass::AL
|
||||
| BidiClass::AN
|
||||
| BidiClass::LRE
|
||||
| BidiClass::RLE
|
||||
| BidiClass::LRO
|
||||
| BidiClass::RLO
|
||||
| BidiClass::PDF
|
||||
| BidiClass::LRI
|
||||
| BidiClass::RLI
|
||||
| BidiClass::FSI
|
||||
| BidiClass::PDI
|
||||
))
|
||||
}
|
||||
|
||||
/// Whether `chars` keep their order and shapes: left-to-right text without
|
||||
/// anything directional, in a language that reads left to right by default.
|
||||
fn unchanged(mut chars: impl Iterator<Item = char>, default_rtl: bool) -> bool {
|
||||
!default_rtl && !chars.any(directional)
|
||||
}
|
||||
|
||||
/// `FormattedBidiReorder.reorder`: `chars` shaped, in visual order, with each
|
||||
/// character's position in the shaped text. A character's style is the one at
|
||||
/// its position in the shaped text, as `SubStringSource` takes it (the text
|
||||
/// shrinks at a lam-alef ligature).
|
||||
fn reorder(chars: &[char], default_rtl: bool) -> Vec<(char, usize)> {
|
||||
let shaped = shape(chars);
|
||||
let plain: String = shaped.iter().collect();
|
||||
// DEFAULT_RTL only changes text without a strong character.
|
||||
let level =
|
||||
(default_rtl && get_base_direction(plain.as_str()) == Direction::Mixed).then(Level::rtl);
|
||||
let info = ParagraphBidiInfo::new(&plain, level);
|
||||
let levels = info.reordered_levels_per_char(0..plain.len());
|
||||
ParagraphBidiInfo::reorder_visual(&levels)
|
||||
.into_iter()
|
||||
.map(|i| {
|
||||
let c = if levels[i].is_rtl() {
|
||||
mirror(shaped[i])
|
||||
} else {
|
||||
shaped[i]
|
||||
};
|
||||
(c, i)
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
/// `Language.getVisualOrder` of a text (its `§` codes already resolved), for
|
||||
/// the current language's direction.
|
||||
pub fn visual(text: &Text) -> Cow<'_, Text> {
|
||||
let default_rtl = crate::lang::bidirectional();
|
||||
if unchanged(text.chars().map(|(c, _)| c), default_rtl) {
|
||||
return Cow::Borrowed(text);
|
||||
}
|
||||
let chars: Vec<char> = text.chars().map(|(c, _)| c).collect();
|
||||
let styles: Vec<_> = text.chars().map(|(_, style)| style).collect();
|
||||
let mut out = Text::default();
|
||||
for (c, at) in reorder(&chars, default_rtl) {
|
||||
match out.spans.last_mut() {
|
||||
Some(span) if span.style == *styles[at] => span.text.push(c),
|
||||
_ => out.spans.push(Span {
|
||||
text: c.to_string(),
|
||||
style: styles[at].clone(),
|
||||
}),
|
||||
}
|
||||
}
|
||||
Cow::Owned(out)
|
||||
}
|
||||
|
||||
/// The same for a plain string.
|
||||
pub fn visual_str(s: &str) -> Cow<'_, str> {
|
||||
let default_rtl = crate::lang::bidirectional();
|
||||
if unchanged(s.chars(), default_rtl) {
|
||||
return Cow::Borrowed(s);
|
||||
}
|
||||
let chars: Vec<char> = s.chars().collect();
|
||||
Cow::Owned(
|
||||
reorder(&chars, default_rtl)
|
||||
.into_iter()
|
||||
.map(|(c, _)| c)
|
||||
.collect(),
|
||||
)
|
||||
}
|
||||
@@ -610,7 +610,9 @@ fn sign_text_calls(
|
||||
.iter()
|
||||
.enumerate()
|
||||
.map(|(i, line)| {
|
||||
// The line that fits, in the order it is drawn (getVisualOrder).
|
||||
let shown = first_line(line, data.max_line_width(), &advance);
|
||||
let shown = crate::bidi::visual_str(&shown).into_owned();
|
||||
TextCall {
|
||||
matrix: m,
|
||||
x: -((width(&shown) as i32) / 2) as f32,
|
||||
|
||||
@@ -7,7 +7,12 @@ use crate::{
|
||||
items::{ItemStack, Items},
|
||||
};
|
||||
use serde_json::Value;
|
||||
use std::{collections::HashSet, fs, path::Path, sync::Arc};
|
||||
use std::{
|
||||
collections::{HashMap, HashSet},
|
||||
fs,
|
||||
path::Path,
|
||||
sync::{Arc, OnceLock},
|
||||
};
|
||||
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
||||
pub enum Category {
|
||||
@@ -94,6 +99,24 @@ pub fn effect(id: &str) -> Option<&'static EffectDef> {
|
||||
let id = id.trim_start_matches("minecraft:");
|
||||
EFFECTS.iter().find(|e| e.id == id)
|
||||
}
|
||||
/// Namespaced effect ids for mob effect storage.
|
||||
pub fn namespaced(id: &'static str) -> &'static str {
|
||||
static IDS: OnceLock<HashMap<&'static str, &'static str>> = OnceLock::new();
|
||||
IDS.get_or_init(|| {
|
||||
EFFECTS
|
||||
.iter()
|
||||
.map(|e| {
|
||||
(
|
||||
e.id,
|
||||
&*Box::leak(format!("minecraft:{}", e.id).into_boxed_str()),
|
||||
)
|
||||
})
|
||||
.collect()
|
||||
})
|
||||
.get(id)
|
||||
.copied()
|
||||
.unwrap_or(id)
|
||||
}
|
||||
/// A `Potion`: id, name suffix (`item.minecraft.potion.effect.<name>`) and
|
||||
/// effects as (effect, duration ticks, amplifier).
|
||||
#[derive(Clone, Copy, Debug)]
|
||||
|
||||
@@ -35,6 +35,23 @@ impl Mode {
|
||||
Self::Redstone => "minecraft:command_block",
|
||||
}
|
||||
}
|
||||
/// `Enum.ordinal`, which `ServerboundSetCommandBlockPacket` writes.
|
||||
pub fn ordinal(self) -> i32 {
|
||||
match self {
|
||||
Self::Sequence => 0,
|
||||
Self::Auto => 1,
|
||||
Self::Redstone => 2,
|
||||
}
|
||||
}
|
||||
/// The mode of a packet's ordinal; `handleSetCommandBlock` treats any
|
||||
/// other value as a plain command block (its `default` branch).
|
||||
pub fn from_ordinal(ordinal: i32) -> Self {
|
||||
match ordinal {
|
||||
0 => Self::Sequence,
|
||||
1 => Self::Auto,
|
||||
_ => Self::Redstone,
|
||||
}
|
||||
}
|
||||
/// `CycleButton`: the next value, wrapping round.
|
||||
pub fn next(self) -> Self {
|
||||
match self {
|
||||
|
||||
@@ -179,13 +179,14 @@ impl PlayerEffects {
|
||||
self.list.clear();
|
||||
}
|
||||
/// The effects of a saved player (`Player.active_effects`: id,
|
||||
/// duration, amplifier, ambient, visible).
|
||||
/// duration, amplifier, ambient, particles visible).
|
||||
pub fn from_saved(saved: &[(String, i32, i32, bool, bool)]) -> Self {
|
||||
let mut effects = Self::default();
|
||||
for (id, duration, amplifier, ambient, _) in saved {
|
||||
for (id, duration, amplifier, ambient, particles) in saved {
|
||||
if let Some(def) = brewing::effect(id) {
|
||||
let mut e = EffectInstance::new(def.id, *duration, *amplifier);
|
||||
e.ambient = *ambient;
|
||||
e.visible = *particles;
|
||||
effects.add(e);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -582,17 +582,21 @@ impl Experience {
|
||||
7 + level * 2
|
||||
}
|
||||
}
|
||||
/// Returns true when a level was gained.
|
||||
pub fn give(&mut self, points: i32) -> bool {
|
||||
let start = self.level;
|
||||
/// Returns the first multiple of 5 the player reached, where each level
|
||||
/// gained would play the level-up sound (`Player.giveExperienceLevels`).
|
||||
pub fn give(&mut self, points: i32) -> Option<i32> {
|
||||
let mut sound_level = None;
|
||||
self.progress += points as f32 / Self::needed(self.level) as f32;
|
||||
self.total = self.total.saturating_add(points).max(0);
|
||||
while self.progress >= 1.0 {
|
||||
self.progress = (self.progress - 1.0) * Self::needed(self.level) as f32;
|
||||
self.level += 1;
|
||||
if self.level % 5 == 0 {
|
||||
sound_level.get_or_insert(self.level);
|
||||
}
|
||||
self.progress /= Self::needed(self.level) as f32;
|
||||
}
|
||||
self.level > start
|
||||
sound_level
|
||||
}
|
||||
}
|
||||
/// A painting (`Painting`, a `HangingEntity`): the block it hangs in, the
|
||||
|
||||
@@ -203,7 +203,7 @@ fn crumbling_uv(uv: vec2<f32>, packed_origin: vec2<f32>, sprite_size: vec2<f32>,
|
||||
// lit texture, then the lightmap, then the glint and fog.
|
||||
color = vec4(mix(in.overlay_color.rgb, color.rgb, in.overlay_color.a), color.a);
|
||||
color *= in.light_color;
|
||||
if in.overlay == 7u || in.overlay == 8u {
|
||||
if in.overlay == 7u || in.overlay == 8u || in.overlay == 33u {
|
||||
// Enchantment glint (overlay 7, 8 with the hurt tint): the purple
|
||||
// enchanted_glint sheen drifting diagonally over the item's shape.
|
||||
let px = in.uv * vec2<f32>(textureDimensions(atlas));
|
||||
@@ -219,11 +219,32 @@ fn crumbling_uv(uv: vec2<f32>, packed_origin: vec2<f32>, sprite_size: vec2<f32>,
|
||||
return vec4(mix(color.rgb, globals.fog_color.rgb, f * globals.fog_color.a), color.a);
|
||||
}
|
||||
|
||||
// rendertype_outline.fsh: the vertex colour (the team colour; outline vertices carry no
|
||||
// normal, so no cardinal light) wherever the texture is not fully clear.
|
||||
@fragment fn fs_outline(in: Out) -> @location(0) vec4<f32> {
|
||||
if textureSample(atlas, atlas_sampler, in.uv).a == 0.0 { discard; }
|
||||
return vec4(in.color.rgb, 1.0);
|
||||
}
|
||||
|
||||
@fragment fn fs_celestial(in: Out) -> @location(0) vec4<f32> {
|
||||
let color = textureSample(atlas, atlas_sampler, in.uv) * in.color * in.light_color;
|
||||
return color;
|
||||
}
|
||||
|
||||
// RenderPipelines.ENERGY_SWIRL (entity.fsh with EMISSIVE and NO_OVERLAY): the sprite, scrolled by
|
||||
// the UV and repeated within itself, times the layer colour 0xFF808080, unlit; the pipeline adds
|
||||
// it to the frame. The light bytes carry the sprite's origin and the colour bytes its size, both
|
||||
// as breaking::stage_light packs them.
|
||||
@fragment fn fs_energy_swirl(in: Out) -> @location(0) vec4<f32> {
|
||||
let bytes = round(in.color * 255.0);
|
||||
let size = vec2(bytes.x + bytes.y * 256.0, bytes.z + bytes.w * 256.0) / 65535.0;
|
||||
let color = textureSample(atlas, atlas_sampler, crumbling_uv(in.uv, in.wrap, size, textureDimensions(atlas)));
|
||||
if color.a < 0.1 { discard; }
|
||||
let f = max(linear_fog(in.spherical, globals.fog.x, globals.fog.y),
|
||||
linear_fog(in.cylindrical, globals.fog.z, globals.fog.w));
|
||||
return vec4(mix(color.rgb * (128.0 / 255.0), globals.fog_color.rgb, f * globals.fog_color.a), color.a);
|
||||
}
|
||||
|
||||
// RenderPipelines.WATER_MASK: depth only (the pipeline masks the colour off).
|
||||
@fragment fn fs_water_mask() -> @location(0) vec4<f32> {
|
||||
return vec4(0.0);
|
||||
|
||||
@@ -187,6 +187,31 @@ pub fn mob_to_nbt(m: &Mob, uuid: [i32; 4], raw: Option<&Tag>) -> Tag {
|
||||
map.remove(&horizon_world::nbt::JavaString::from(key));
|
||||
}
|
||||
}
|
||||
// LivingEntity.addAdditionalSaveData: `active_effects` only when there are
|
||||
// any (MobEffectInstance.CODEC; a mob's effects are never ambient, and one
|
||||
// given with `hideParticles` shows neither particles nor its icon).
|
||||
if m.extra.effects.is_empty() {
|
||||
map.remove(&horizon_world::nbt::JavaString::from("active_effects"));
|
||||
} else {
|
||||
let effects = m
|
||||
.extra
|
||||
.effects
|
||||
.iter()
|
||||
.map(|&(effect, duration, amplifier)| {
|
||||
let shown = !m.extra.hidden_effects.contains(&effect);
|
||||
let mut e = vec![
|
||||
("id", string(crate::brewing::namespaced(effect))),
|
||||
("amplifier", Tag::Byte(amplifier.clamp(0, 255) as u8 as i8)),
|
||||
("duration", Tag::Int(duration)),
|
||||
];
|
||||
if !shown {
|
||||
e.push(("show_particles", Tag::Byte(0)));
|
||||
}
|
||||
e.push(("show_icon", Tag::Byte(i8::from(shown))));
|
||||
Tag::compound(e)
|
||||
});
|
||||
map.insert("active_effects".into(), Tag::List(effects.collect()));
|
||||
}
|
||||
map.insert(
|
||||
"Invulnerable".into(),
|
||||
Tag::Byte(i8::from(m.invulnerable_flag)),
|
||||
@@ -278,6 +303,18 @@ pub fn mob_to_nbt(m: &Mob, uuid: [i32; 4], raw: Option<&Tag>) -> Tag {
|
||||
map.entry("sound_variant".into())
|
||||
.or_insert_with(|| string("minecraft:classic"));
|
||||
}
|
||||
"minecraft:panda" => {
|
||||
use crate::mobs::panda::GENES;
|
||||
for (key, gene) in [
|
||||
("MainGene", m.extra.variant),
|
||||
("HiddenGene", m.extra.variant2),
|
||||
] {
|
||||
map.insert(
|
||||
key.into(),
|
||||
string(GENES[usize::from(gene).min(GENES.len() - 1)]),
|
||||
);
|
||||
}
|
||||
}
|
||||
"minecraft:piglin" | "minecraft:piglin_brute" | "minecraft:hoglin" => {
|
||||
// Only Piglin.addAdditionalSaveData writes CannotHunt.
|
||||
if id == "minecraft:piglin_brute" {
|
||||
@@ -426,7 +463,36 @@ pub fn mob_to_nbt(m: &Mob, uuid: [i32; 4], raw: Option<&Tag>) -> Tag {
|
||||
crate::mobs::villager::write_nbt(m, map);
|
||||
}
|
||||
"minecraft:wandering_trader" => put_offers(map, m),
|
||||
"minecraft:axolotl" => write_axolotl_memories(m, map),
|
||||
"minecraft:axolotl" => {
|
||||
write_axolotl_memories(m, map);
|
||||
map.insert(
|
||||
"Variant".into(),
|
||||
Tag::Int(i32::from(m.extra.variant.min(4))),
|
||||
);
|
||||
}
|
||||
// Rabbit.addAdditionalSaveData (the legacy id; 99 is the killer bunny).
|
||||
"minecraft:rabbit" => {
|
||||
map.insert("RabbitType".into(), Tag::Int(i32::from(m.extra.variant)));
|
||||
}
|
||||
"minecraft:parrot" => {
|
||||
map.insert(
|
||||
"Variant".into(),
|
||||
Tag::Int(i32::from(m.extra.variant.min(4))),
|
||||
);
|
||||
}
|
||||
"minecraft:salmon" => {
|
||||
let size = SALMON_SIZES[usize::from(m.extra.variant).min(2)];
|
||||
map.insert("type".into(), string(size));
|
||||
}
|
||||
// TropicalFish.addAdditionalSaveData: the packed variant.
|
||||
"minecraft:tropical_fish" => {
|
||||
let pattern = m.extra.variant.min(11);
|
||||
let packed = i32::from(pattern >= 6)
|
||||
| i32::from(pattern % 6) << 8
|
||||
| i32::from(m.extra.variant2 & 15) << 16
|
||||
| i32::from(m.extra.variant3 & 15) << 24;
|
||||
map.insert("Variant".into(), Tag::Int(packed));
|
||||
}
|
||||
"minecraft:allay" => {
|
||||
// InventoryCarrier: the fetched stack; the liked player memory.
|
||||
let mut inv = Vec::new();
|
||||
@@ -854,6 +920,25 @@ pub fn mob_from_nbt(tag: &Tag, types: &HashMap<String, Arc<MobType>>, id: u32) -
|
||||
m.no_gravity = flag(tag, "NoGravity");
|
||||
m.glowing = flag(tag, "Glowing");
|
||||
m.ticks_frozen = number(tag.get("TicksFrozen")).unwrap_or(0) as i32;
|
||||
// LivingEntity.readAdditionalSaveData: the saved `active_effects`, the last
|
||||
// entry of an effect winning.
|
||||
if let Some(Tag::List(effects)) = tag.get("active_effects") {
|
||||
for e in effects {
|
||||
let Some(def) = text(e.get("id")).and_then(|id| crate::brewing::effect(&id)) else {
|
||||
continue;
|
||||
};
|
||||
let effect = crate::brewing::namespaced(def.id);
|
||||
let duration = number(e.get("duration")).unwrap_or(0) as i32;
|
||||
let amplifier = number(e.get("amplifier")).unwrap_or(0) as i32;
|
||||
m.extra.effects.retain(|x| x.0 != effect);
|
||||
m.extra.effects.push((effect, duration, amplifier));
|
||||
// Details.CODEC: `show_particles` defaults to true.
|
||||
m.extra.hidden_effects.retain(|h| *h != effect);
|
||||
if number(e.get("show_particles")) == Some(0) {
|
||||
m.extra.hidden_effects.push(effect);
|
||||
}
|
||||
}
|
||||
}
|
||||
m.silent = flag(tag, "Silent");
|
||||
m.invulnerable_flag = flag(tag, "Invulnerable");
|
||||
if m.species() == crate::mobs::Species::Bat {
|
||||
@@ -950,6 +1035,11 @@ pub fn mob_from_nbt(tag: &Tag, types: &HashMap<String, Arc<MobType>>, id: u32) -
|
||||
.unwrap_or(0) as u8;
|
||||
}
|
||||
}
|
||||
if m.species() == crate::mobs::Species::Panda {
|
||||
let gene = |key| text(tag.get(key)).map_or(0, |n| crate::mobs::panda::gene_from_name(&n));
|
||||
m.extra.variant = gene("MainGene");
|
||||
m.extra.variant2 = gene("HiddenGene");
|
||||
}
|
||||
if m.species() == crate::mobs::Species::Wolf {
|
||||
let health = m.health;
|
||||
m.extra.owner = crate::save::uuid_from_tag(tag.get("Owner"));
|
||||
@@ -985,6 +1075,7 @@ pub fn mob_from_nbt(tag: &Tag, types: &HashMap<String, Arc<MobType>>, id: u32) -
|
||||
if m.species() == crate::mobs::Species::Axolotl {
|
||||
restore_axolotl_memories(&mut m, tag);
|
||||
}
|
||||
read_variant(&mut m, tag);
|
||||
m.fire_ticks = number(tag.get("Fire")).unwrap_or(0) as i32;
|
||||
m.air = number(tag.get("Air")).unwrap_or(300) as i32;
|
||||
if m.species() == crate::mobs::Species::Allay {
|
||||
@@ -2166,6 +2257,41 @@ fn write_axolotl_memories(m: &Mob, map: &mut Compound) {
|
||||
}
|
||||
}
|
||||
}
|
||||
/// The variant tags `Axolotl`, `Rabbit`, `Parrot`, `Salmon` and
|
||||
/// `TropicalFish` read; a missing tag is the default variant (a medium
|
||||
/// salmon, otherwise index 0).
|
||||
fn read_variant(m: &mut Mob, tag: &Tag) {
|
||||
use crate::mobs::Species as S;
|
||||
let int = |key| number(tag.get(key)).unwrap_or(0) as i32;
|
||||
match m.species() {
|
||||
S::Axolotl | S::Parrot => m.extra.variant = int("Variant").clamp(0, 4) as u8,
|
||||
// Rabbit.Variant.LEGACY_CODEC: an unknown id is brown.
|
||||
S::Rabbit => {
|
||||
m.extra.variant = match int("RabbitType") {
|
||||
v @ (0..=5 | 99) => v as u8,
|
||||
_ => 0,
|
||||
}
|
||||
}
|
||||
S::Salmon => {
|
||||
m.extra.variant = text(tag.get("type"))
|
||||
.and_then(|t| SALMON_SIZES.iter().position(|v| *v == t))
|
||||
.unwrap_or(1) as u8;
|
||||
}
|
||||
S::TropicalFish => {
|
||||
let packed = int("Variant");
|
||||
let (size, index) = (packed & 0xFF, (packed >> 8) & 0xFF);
|
||||
// TropicalFish.Pattern.byId: an unknown id is a kob.
|
||||
m.extra.variant = if size <= 1 && index < 6 {
|
||||
(size * 6 + index) as u8
|
||||
} else {
|
||||
0
|
||||
};
|
||||
m.extra.variant2 = ((packed >> 16) & 15) as u8;
|
||||
m.extra.variant3 = ((packed >> 24) & 15) as u8;
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
fn restore_axolotl_memories(m: &mut Mob, tag: &Tag) {
|
||||
let memories = tag.get("Brain").and_then(|b| b.get("memories"));
|
||||
let ttl = memories
|
||||
|
||||
@@ -889,12 +889,17 @@ impl Font {
|
||||
if s.contains('§') {
|
||||
return self.width_text(&Text::legacy(s));
|
||||
}
|
||||
s.chars().map(|c| self.glyph(c).advance).sum::<f32>().ceil()
|
||||
crate::bidi::visual_str(s)
|
||||
.chars()
|
||||
.map(|c| self.glyph(c).advance)
|
||||
.sum::<f32>()
|
||||
.ceil()
|
||||
}
|
||||
/// Font.width of a component: `§` codes in the spans are formatting,
|
||||
/// not glyphs.
|
||||
/// not glyphs. Right-to-left text is measured as it is drawn, shaped.
|
||||
pub fn width_text(&self, t: &Text) -> f32 {
|
||||
t.resolved()
|
||||
let t = t.resolved();
|
||||
crate::bidi::visual(&t)
|
||||
.chars()
|
||||
.map(|(c, s)| self.advance(c, s))
|
||||
.sum::<f32>()
|
||||
|
||||
@@ -63,9 +63,9 @@ mod frames;
|
||||
mod friends;
|
||||
mod functions;
|
||||
mod game_mode;
|
||||
mod game_rules_screen;
|
||||
mod gamemode_switcher;
|
||||
mod ghasts;
|
||||
mod glowing;
|
||||
mod golems;
|
||||
mod growth;
|
||||
mod hand;
|
||||
@@ -128,6 +128,7 @@ mod packs;
|
||||
mod pad;
|
||||
mod pad_menu;
|
||||
mod paintings;
|
||||
mod pandas;
|
||||
mod passengers;
|
||||
mod persist;
|
||||
mod perspective;
|
||||
@@ -1341,6 +1342,8 @@ struct Game<'a> {
|
||||
/// Runs of `entity_vertices` by entity, for a shader pack's per-entity draws; recorded only
|
||||
/// while `pack_ids` is set (game/shader_packs.rs).
|
||||
entity_spans: std::cell::RefCell<Vec<horizon_client::gfx::pack_draw::EntitySpan>>,
|
||||
/// The glowing entities' silhouettes this frame (game/glowing.rs).
|
||||
glowing_vertices: std::cell::RefCell<Vec<Vertex>>,
|
||||
/// The shader pack's entity and block entity ids, while one draws entities.
|
||||
pack_ids: Option<shader_packs::PackIds>,
|
||||
started: Instant,
|
||||
@@ -2037,9 +2040,11 @@ impl Game<'_> {
|
||||
/// item in use (LocalPlayer.modifyInput), the helmet's Respiration
|
||||
/// (decreaseAirSupply), the wings, leather boots on powder snow
|
||||
/// (PowderSnowBlock.canEntityWalkOnPowderSnow), freeze-immune wear
|
||||
/// (LivingEntity.canFreeze) and the dimension's FAST_LAVA.
|
||||
/// (LivingEntity.canFreeze), the dimension's FAST_LAVA and the sleeping
|
||||
/// pose.
|
||||
pub(crate) fn sync_player_body(&mut self) {
|
||||
let glider = self.glider_worn();
|
||||
let sleeping = self.sleeping.is_some();
|
||||
let item_use_speed = self
|
||||
.using
|
||||
.and_then(|(slot, _)| {
|
||||
@@ -2067,6 +2072,7 @@ impl Game<'_> {
|
||||
player.item_use_speed = item_use_speed;
|
||||
player.respiration = respiration;
|
||||
player.glider = glider;
|
||||
let _ = player.set_sleeping(sleeping);
|
||||
player.leather_boots = leather_boots;
|
||||
player.freeze_immune = freeze_immune;
|
||||
player.fluids.fast_lava = self.dim == Dim::Nether;
|
||||
@@ -2454,6 +2460,15 @@ impl Game<'_> {
|
||||
no_physics,
|
||||
&self.res.motion().fluids,
|
||||
);
|
||||
// Entity.applyEffectsFromBlocks: bubble columns lift or drag the item.
|
||||
horizon_client::motion::bubble_column_push(
|
||||
&self.world,
|
||||
self.pack.assets.registry,
|
||||
self.res.facts(),
|
||||
self.collision,
|
||||
item.bounds(),
|
||||
&mut item.velocity,
|
||||
);
|
||||
cells.moved(i, item.previous, item.position);
|
||||
// ItemEntity.tick: merge every 2 ticks while moving between
|
||||
// blocks, every 40 otherwise, before the lifetime check.
|
||||
@@ -2990,6 +3005,16 @@ impl Game<'_> {
|
||||
data,
|
||||
} => self.sculk_charge_event(pos, data),
|
||||
LevelEvent::LevelEvent { id: 3007, pos, .. } => self.shriek_event(pos),
|
||||
// LevelEventHandler 2019 and 2020: the crack on a face of the
|
||||
// block being mined, with its hit sound for 2020.
|
||||
LevelEvent::LevelEvent {
|
||||
id: id @ (2019 | 2020),
|
||||
pos,
|
||||
data,
|
||||
} => {
|
||||
let face = horizon_core::geometry::Direction::ALL[data.clamp(0, 5) as usize];
|
||||
self.breaking_block_effects([pos.x, pos.y, pos.z], face.normal(), id == 2020);
|
||||
}
|
||||
LevelEvent::LevelEvent { id: 2004, pos, .. } => {
|
||||
self.spawner_burst([pos.x, pos.y, pos.z])
|
||||
}
|
||||
@@ -4241,6 +4266,17 @@ impl Game<'_> {
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
/// `ServerPlayerGameMode.tick` while a player digs: level event 2020 every
|
||||
/// fourth tick since the START (the hit sound), else 2019 (the crack only),
|
||||
/// for everyone around, the digger included (`levelEvent(null, ...)`).
|
||||
fn destroy_progress_event(&mut self, pos: [i32; 3], face: [i32; 3], ticks: u32) {
|
||||
let id = if ticks.is_multiple_of(4) { 2020 } else { 2019 };
|
||||
let direction = horizon_core::geometry::Direction::ALL
|
||||
.iter()
|
||||
.position(|d| d.normal() == face)
|
||||
.unwrap_or(1);
|
||||
self.level_event_at(id, pos, direction as i32);
|
||||
}
|
||||
fn tick_mining(&mut self) -> Result<()> {
|
||||
if self.destroy_delay > 0 {
|
||||
self.destroy_delay -= 1;
|
||||
@@ -4284,6 +4320,11 @@ impl Game<'_> {
|
||||
let state = self.world.block(hit.pos[0], hit.pos[1], hit.pos[2]);
|
||||
let facts = self.res.facts().get(state);
|
||||
if facts.hardness < 0.0 {
|
||||
// The server still counts its destroy ticks on an unbreakable block.
|
||||
let m = self.mining.as_mut().ok_or("mining")?;
|
||||
m.ticks += 1;
|
||||
let (pos, ticks) = (m.pos, m.ticks);
|
||||
self.destroy_progress_event(pos, hit.face, ticks);
|
||||
return Ok(());
|
||||
}
|
||||
// Player.getDestroySpeed / BlockBehaviour.getDestroyProgress.
|
||||
@@ -4314,26 +4355,15 @@ impl Game<'_> {
|
||||
} else {
|
||||
speed / facts.hardness / if harvest { 30.0 } else { 100.0 }
|
||||
};
|
||||
let hit_sound = facts.sounds.clone();
|
||||
self.swing_arm();
|
||||
let m = self.mining.as_mut().ok_or("mining")?;
|
||||
m.progress += progress;
|
||||
m.ticks += 1;
|
||||
let crack = (m.pos, hit.face);
|
||||
if m.ticks % 4 == 1 {
|
||||
let c = [
|
||||
f64::from(m.pos[0]) + 0.5,
|
||||
f64::from(m.pos[1]) + 0.5,
|
||||
f64::from(m.pos[2]) + 0.5,
|
||||
];
|
||||
self.audio.play(
|
||||
&hit_sound.hit,
|
||||
Some(c),
|
||||
(hit_sound.volume + 1.0) / 8.0,
|
||||
hit_sound.pitch * 0.5,
|
||||
);
|
||||
// An instant break at START sends no destroy events.
|
||||
if m.progress < 1.0 || m.ticks > 1 {
|
||||
let (pos, ticks) = (m.pos, m.ticks);
|
||||
self.destroy_progress_event(pos, hit.face, ticks);
|
||||
}
|
||||
self.crack_particle(crack.0, crack.1, state);
|
||||
let m = self.mining.as_mut().ok_or("mining")?;
|
||||
// ServerPlayerGameMode breaks a player's block at once only when it is instant (START's
|
||||
// insta-mine); otherwise it waits for the client's STOP_DESTROY_BLOCK
|
||||
@@ -4653,7 +4683,6 @@ impl Game<'_> {
|
||||
player.dimensions.eye_height
|
||||
};
|
||||
self.eye += (camera_eye.unwrap_or(eye_height) - self.eye) * 0.5;
|
||||
let crouching = player.pose == horizon_client::pose::Pose::Crouching;
|
||||
// `LivingEntity.baseTick` (Soul Speed) comes before `aiStep` (the
|
||||
// glide's wear): both draw from the level random.
|
||||
if let Some((at, velocity)) = self.tick_soul_speed() {
|
||||
@@ -4689,10 +4718,12 @@ impl Game<'_> {
|
||||
MotionEvent::HurtSound(sound) => self.player_hurt_sound(sound, feet),
|
||||
MotionEvent::Emission {
|
||||
state, in_water, ..
|
||||
} if !crouching => {
|
||||
} => {
|
||||
let sounds = self.res.facts().get(state).sounds.clone();
|
||||
// Entity.playStepSound: the block's step sound at 0.15 volume,
|
||||
// muffled under the swim sound in water.
|
||||
// muffled under the swim sound in water. Whether it plays is
|
||||
// Player.getMovementEmission's call (shift on the ground),
|
||||
// not the pose: a forced crouch still steps.
|
||||
let (volume, pitch) = if in_water {
|
||||
(sounds.volume * 0.05, sounds.pitch * 0.8)
|
||||
} else {
|
||||
@@ -4704,26 +4735,21 @@ impl Game<'_> {
|
||||
self.audio
|
||||
.play("minecraft:entity.player.swim", Some(feet), volume, pitch);
|
||||
}
|
||||
MotionEvent::Chime { volume, pitch } => {
|
||||
self.audio.play(
|
||||
"minecraft:block.amethyst_block.chime",
|
||||
Some(feet),
|
||||
volume,
|
||||
pitch,
|
||||
);
|
||||
}
|
||||
MotionEvent::Drown => {
|
||||
// LivingEntity.makeDrownParticles: eight bubbles moving
|
||||
// with the player.
|
||||
use horizon_client::particles::{ParticleOptions, ParticleType};
|
||||
for _ in 0..8 {
|
||||
let mut triangle = || {
|
||||
let r = self.particles.random();
|
||||
r.next_double() - r.next_double()
|
||||
};
|
||||
let p = [
|
||||
feet[0] + triangle(),
|
||||
feet[1] + triangle(),
|
||||
feet[2] + triangle(),
|
||||
];
|
||||
self.particles.add(
|
||||
&ParticleOptions::simple(ParticleType::Bubble),
|
||||
p,
|
||||
player_velocity,
|
||||
);
|
||||
// The guests see the entity event 67 too.
|
||||
if let Some(host) = &self.lan {
|
||||
host.hub
|
||||
.entity_event(host.host_entity, combat::DROWN_EVENT, feet);
|
||||
}
|
||||
self.drown_particles(feet, player_velocity);
|
||||
}
|
||||
MotionEvent::BubbleColumnParticle { bubble, position } => {
|
||||
// ServerLevel.sendParticles: SPLASH (speed 1) or BUBBLE
|
||||
@@ -4826,6 +4852,7 @@ impl Game<'_> {
|
||||
}
|
||||
}
|
||||
self.powder_snow_fire()?;
|
||||
self.rain_extinguish_in_frame();
|
||||
self.tick_totem();
|
||||
let Some(player) = self.player.as_mut() else {
|
||||
return Ok(());
|
||||
@@ -9710,16 +9737,13 @@ impl Game<'_> {
|
||||
/// sound every fifth level (at most every 100 ticks).
|
||||
pub(super) fn give_orb_points(&mut self, gained: u32) {
|
||||
let at = self.eye_position();
|
||||
if self.experience.give(gained as i32)
|
||||
&& self.experience.level % 5 == 0
|
||||
// Player.giveExperienceLevels runs per level, so the first multiple of 5
|
||||
// crossed plays the sound (and starts the cooldown for the rest).
|
||||
if let Some(level) = self.experience.give(gained as i32)
|
||||
&& self.ticks > self.last_level_up + 100
|
||||
{
|
||||
self.last_level_up = self.ticks;
|
||||
let volume = if self.experience.level > 30 {
|
||||
1.0
|
||||
} else {
|
||||
self.experience.level as f32 / 30.0
|
||||
};
|
||||
let volume = if level > 30 { 1.0 } else { level as f32 / 30.0 };
|
||||
self.audio
|
||||
.play("minecraft:entity.player.levelup", at, volume * 0.75, 1.0);
|
||||
}
|
||||
@@ -10089,6 +10113,14 @@ impl Game<'_> {
|
||||
};
|
||||
Some(t < 0.15)
|
||||
}
|
||||
/// `Level.isRainingAt`: it rains, the sky light is 15, nothing that stops
|
||||
/// motion is above, and the biome's precipitation there is rain.
|
||||
fn raining_at(&self, p: BlockPos) -> bool {
|
||||
self.world.weather.levels(1.0).0 > 0.2
|
||||
&& self.world.light(p.x, p.y, p.z) >> 4 == 15
|
||||
&& self.rain_height(p.x, p.z) <= p.y
|
||||
&& self.precipitation(p.x, p.y, p.z) == Some(false)
|
||||
}
|
||||
/// `getHeight(Heightmap.Types.MOTION_BLOCKING, x, z)`: one above the
|
||||
/// highest block in `blocks_motion_in_heightmap` or holding a fluid.
|
||||
fn rain_height(&self, x: i32, z: i32) -> i32 {
|
||||
@@ -10125,12 +10157,15 @@ impl Game<'_> {
|
||||
} else {
|
||||
("minecraft:weather.rain", 0.2, 1.0)
|
||||
};
|
||||
self.audio.play(
|
||||
name,
|
||||
Some([f64::from(x) + 0.5, f64::from(top), f64::from(z) + 0.5]),
|
||||
volume,
|
||||
pitch,
|
||||
);
|
||||
// The Horizon Weather option off silences the rain too.
|
||||
if self.settings.horizon.weather {
|
||||
self.audio.play(
|
||||
name,
|
||||
Some([f64::from(x) + 0.5, f64::from(top), f64::from(z) + 0.5]),
|
||||
volume,
|
||||
pitch,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -10673,6 +10708,9 @@ impl Game<'_> {
|
||||
let level = &mut self.level;
|
||||
level.creative = self.creative;
|
||||
level.game_mode = mode;
|
||||
// PrimaryLevelData.setDifficulty: /difficulty and the options button change it.
|
||||
level.difficulty =
|
||||
horizon_client::title::DIFFICULTIES[usize::from(self.ui.difficulty) & 3].into();
|
||||
level.time = self.world.game_time;
|
||||
level.weather = self.world.weather;
|
||||
level.day_time = self.world.day_time;
|
||||
@@ -10948,7 +10986,11 @@ impl Game<'_> {
|
||||
let debug_crosshair = horizon_client::gizmos::camera_relative(&crosshair.on_top, at, view);
|
||||
// LevelExtractor.extractBlockOutline: model translucency controls draw order.
|
||||
let mut outline_after_terrain = false;
|
||||
let outline = if self.screen == Screen::Playing && !self.spectator() {
|
||||
// GameRenderer.shouldRenderBlockOutline: not with the HUD hidden (F1), and only for a
|
||||
// player who may build. Adventure players only get one with a CAN_BREAK or
|
||||
// CAN_PLACE_ON item and spectators with a menu block; Horizon reads neither the
|
||||
// components nor opens menus for spectators, so those never show it.
|
||||
let outline = if self.screen == Screen::Playing && !self.ui.hide_gui && self.may_build() {
|
||||
self.pick()
|
||||
.map(|h| {
|
||||
let state = self.world.block(h.pos[0], h.pos[1], h.pos[2]);
|
||||
@@ -11033,6 +11075,7 @@ impl Game<'_> {
|
||||
let mut entities = std::mem::take(&mut self.entity_vertices);
|
||||
entities.clear();
|
||||
self.see_through_vertices.clear();
|
||||
self.glowing_vertices.get_mut().clear();
|
||||
if !title {
|
||||
entities = horizon_client::spans::time("entity_geometry", || {
|
||||
self.entity_geometry(camera, alpha, entities)
|
||||
@@ -11100,6 +11143,7 @@ impl Game<'_> {
|
||||
// map_patches any earlier leaves them a frame behind.
|
||||
hud.patches.append(&mut self.map_patches);
|
||||
let entity_spans = std::mem::take(self.entity_spans.get_mut());
|
||||
let glowing = std::mem::take(self.glowing_vertices.get_mut());
|
||||
let frame = Frame {
|
||||
weather: &weather,
|
||||
weather_time: horizon_client::weather::Time::new(self.world.game_time, alpha as f32),
|
||||
@@ -11144,6 +11188,7 @@ impl Game<'_> {
|
||||
dark_disc: self.dark_disc(camera),
|
||||
outline,
|
||||
outline_after_terrain,
|
||||
outline_high_contrast: self.settings.file.bool("highContrastBlockOutline"),
|
||||
debug_lines: &debug_lines,
|
||||
debug_lines_on_top: &debug_lines_on_top,
|
||||
debug_crosshair: &debug_crosshair,
|
||||
@@ -11165,6 +11210,7 @@ impl Game<'_> {
|
||||
}),
|
||||
entities: &entities,
|
||||
entity_spans: &entity_spans,
|
||||
glowing: &glowing,
|
||||
dynamic_lights: &self.dynamic_lights,
|
||||
lightning: horizon_client::gfx::lightning::Input {
|
||||
bolts: &bolts,
|
||||
@@ -11203,7 +11249,7 @@ impl Game<'_> {
|
||||
}),
|
||||
ticks: self.ticks,
|
||||
darkness: self.darkness_lighting(alpha as f32),
|
||||
night_vision: self.night_vision_intensity(),
|
||||
night_vision: self.night_vision_intensity(alpha as f32),
|
||||
block_flicker: self.fog.block_light_flicker,
|
||||
boss_darken: self
|
||||
.boss_overlay
|
||||
@@ -11220,6 +11266,7 @@ impl Game<'_> {
|
||||
// Restore storage even when rendering fails (for example, a lost surface).
|
||||
self.entity_vertices = entities;
|
||||
*self.entity_spans.get_mut() = entity_spans;
|
||||
*self.glowing_vertices.get_mut() = glowing;
|
||||
self.see_through_vertices = see_through;
|
||||
result
|
||||
}
|
||||
@@ -12108,7 +12155,8 @@ impl ApplicationHandler for Game<'_> {
|
||||
let (action, sound) = self.title.as_mut().map_or((Action::None, false), |t| {
|
||||
let clicked = t.click(self.cursor.0, self.cursor.1, (b.width, b.height));
|
||||
t.press_field(atlas, self.cursor, (b.width, b.height), shift);
|
||||
clicked
|
||||
let rules = t.press_rules(atlas, self.cursor, (b.width, b.height));
|
||||
(clicked.0, clicked.1 || rules)
|
||||
});
|
||||
self.save_title_options();
|
||||
if sound {
|
||||
@@ -12877,6 +12925,7 @@ impl<'a> Game<'a> {
|
||||
luminance: None,
|
||||
see_through_vertices: Vec::new(),
|
||||
entity_spans: Default::default(),
|
||||
glowing_vertices: Default::default(),
|
||||
pack_ids: None,
|
||||
health_hud: hud_layout::HealthHud::default(),
|
||||
started: now,
|
||||
@@ -13851,6 +13900,7 @@ fn loading_frame(hud: &HudBatch) -> Frame<'_> {
|
||||
dark_disc: false,
|
||||
outline: Vec::new(),
|
||||
outline_after_terrain: false,
|
||||
outline_high_contrast: false,
|
||||
debug_lines: &[],
|
||||
debug_lines_on_top: &[],
|
||||
debug_crosshair: &[],
|
||||
@@ -13858,6 +13908,7 @@ fn loading_frame(hud: &HudBatch) -> Frame<'_> {
|
||||
nether_lighting: false,
|
||||
entities: &[],
|
||||
entity_spans: &[],
|
||||
glowing: &[],
|
||||
dynamic_lights: &[],
|
||||
clouds: None,
|
||||
lightning: Default::default(),
|
||||
|
||||
@@ -547,7 +547,7 @@ impl Game<'_> {
|
||||
});
|
||||
}
|
||||
/// `Entity.isInRain`: rain at the block position or at the top of the box.
|
||||
fn in_rain(&mut self, at: Vec3, top: f64) -> bool {
|
||||
pub(super) fn in_rain(&mut self, at: Vec3, top: f64) -> bool {
|
||||
let p = BlockPos::new(
|
||||
at.x.floor() as i32,
|
||||
at.y.floor() as i32,
|
||||
|
||||
@@ -801,7 +801,11 @@ impl Game<'_> {
|
||||
pub(super) fn load_world_state(&mut self) {
|
||||
self.ui.rules = GameRules::load(&self.world_root);
|
||||
self.ui.clock = WorldClock::default();
|
||||
self.ui.difficulty = read_difficulty(&self.world_root).unwrap_or(2);
|
||||
// PrimaryLevelData: the difficulty comes back from level.dat's difficulty_settings.
|
||||
self.ui.difficulty = horizon_client::title::DIFFICULTIES
|
||||
.iter()
|
||||
.position(|d| *d == self.level.difficulty)
|
||||
.map_or(2, |i| i as u8);
|
||||
if let Ok(bytes) = std::fs::read(self.world_root.join("data/minecraft/world_clocks.dat"))
|
||||
&& let Ok(raw) = horizon_world::compression::Compression::Gzip.decode(&bytes, 1 << 20)
|
||||
&& let Ok((root, _)) =
|
||||
@@ -826,9 +830,9 @@ impl Game<'_> {
|
||||
self.build_registries();
|
||||
self.sync_rules();
|
||||
}
|
||||
/// Save the clock and the difficulty beside level.dat (Level.write saves
|
||||
/// the game rules: a second writer of game_rules.dat could leave it with
|
||||
/// the rules loaded when the world opened).
|
||||
/// Save the clock beside level.dat (Level.write saves the game rules: a
|
||||
/// second writer of game_rules.dat could leave it with the rules loaded
|
||||
/// when the world opened).
|
||||
pub(super) fn save_world_state(&mut self) {
|
||||
if self.world_root.as_os_str().is_empty() {
|
||||
return;
|
||||
@@ -876,9 +880,6 @@ impl Game<'_> {
|
||||
if let Err(e) = horizon_client::save::write_dat(&path, root) {
|
||||
log::warn!("the world clocks were not written: {e}");
|
||||
}
|
||||
if let Err(e) = write_difficulty(&self.world_root, self.ui.difficulty) {
|
||||
log::warn!("the difficulty was not written: {e}");
|
||||
}
|
||||
}
|
||||
/// Death message in chat (CombatTracker.getDeathMessage, as far as the
|
||||
/// cause is known to the client).
|
||||
@@ -988,34 +989,6 @@ pub fn screenshot_name(dir: &std::path::Path) -> String {
|
||||
n += 1;
|
||||
}
|
||||
}
|
||||
fn read_difficulty(world: &std::path::Path) -> Option<u8> {
|
||||
let bytes = std::fs::read(world.join("level.dat")).ok()?;
|
||||
let raw = horizon_world::compression::Compression::Gzip
|
||||
.decode(&bytes, 64 << 20)
|
||||
.ok()?;
|
||||
let (root, _) =
|
||||
horizon_world::nbt::read_named(&raw, horizon_world::nbt::Limits::default()).ok()?;
|
||||
match root.tag.get("Data")?.get("Difficulty")? {
|
||||
horizon_world::nbt::Tag::Byte(b) => Some((*b).clamp(0, 3) as u8),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
/// Patch `Data.Difficulty` in level.dat (Level::write stores the default).
|
||||
fn write_difficulty(world: &std::path::Path, difficulty: u8) -> horizon_client::Result<()> {
|
||||
use horizon_world::{compression::Compression, nbt};
|
||||
let path = world.join("level.dat");
|
||||
let bytes = std::fs::read(&path)?;
|
||||
let raw = Compression::Gzip.decode(&bytes, 64 << 20)?;
|
||||
let (mut root, _) = nbt::read_named(&raw, nbt::Limits::default()).map_err(|e| e.to_string())?;
|
||||
if let Some(data) = root.tag.get_mut("Data")
|
||||
&& let Some(d) = data.get_mut("Difficulty")
|
||||
{
|
||||
*d = nbt::Tag::Byte(difficulty as i8);
|
||||
}
|
||||
let out = nbt::write_named(&root, nbt::Limits::default()).map_err(|e| e.to_string())?;
|
||||
std::fs::write(&path, Compression::Gzip.encode(&out)?)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// `IntegratedServer.getProfilePermissions` for the owner: `OWNER` (4) when
|
||||
/// commands are allowed, else `ALL` (0).
|
||||
|
||||
@@ -37,6 +37,7 @@ impl Game<'_> {
|
||||
}
|
||||
}
|
||||
self.eye_particles();
|
||||
self.panda_effects();
|
||||
self.sprint_dust();
|
||||
self.effect_swirls();
|
||||
}
|
||||
|
||||
@@ -1,7 +1,16 @@
|
||||
//! The player's `CombatTracker`: the damage taken lately, who dealt it and
|
||||
//! where the player fell from, turned into the vanilla death message.
|
||||
use super::{Game, chat::PLAYER_NAME};
|
||||
use horizon_client::{health::HealthEvent, motion::MotionEvent, text::Text};
|
||||
use horizon_client::{
|
||||
health::HealthEvent,
|
||||
motion::MotionEvent,
|
||||
particles::{ParticleOptions, ParticleType},
|
||||
text::Text,
|
||||
};
|
||||
use horizon_core::random::RandomSource;
|
||||
|
||||
/// `EntityEvent.DROWN_PARTICLES`: broadcast with the drowning damage.
|
||||
pub(super) const DROWN_EVENT: i8 = 67;
|
||||
|
||||
struct Entry {
|
||||
damage_type: String,
|
||||
@@ -45,6 +54,38 @@ pub(super) struct CombatTracker {
|
||||
fall_before: f64,
|
||||
}
|
||||
impl Game<'_> {
|
||||
/// `LivingEntity.makeDrownParticles`: eight bubbles around the entity,
|
||||
/// moving with it.
|
||||
pub(super) fn drown_particles(&mut self, at: [f64; 3], velocity: [f64; 3]) {
|
||||
let bubble = ParticleOptions::simple(ParticleType::Bubble);
|
||||
for _ in 0..8 {
|
||||
let mut triangle = || {
|
||||
let r = self.particles.random();
|
||||
r.next_double() - r.next_double()
|
||||
};
|
||||
let p = [at[0] + triangle(), at[1] + triangle(), at[2] + triangle()];
|
||||
self.particles.add(&bubble, p, velocity);
|
||||
}
|
||||
}
|
||||
/// Entity event 67 from a server, for this client's own player or an
|
||||
/// entity it draws.
|
||||
pub(super) fn remote_drown(&mut self, id: i32) {
|
||||
let own = self.remote.as_ref().is_some_and(|r| r.entity_id == id);
|
||||
let body = if own {
|
||||
self.player.as_ref().map(|p| {
|
||||
let (at, v) = (p.position, p.velocity);
|
||||
([at.x, at.y, at.z], [v.x, v.y, v.z])
|
||||
})
|
||||
} else {
|
||||
self.mobs.iter().find(|m| m.id == id as u32).map(|m| {
|
||||
let (at, v) = (m.position, m.velocity);
|
||||
([at.x, at.y, at.z], [v.x, v.y, v.z])
|
||||
})
|
||||
};
|
||||
if let Some((at, v)) = body {
|
||||
self.drown_particles(at, v);
|
||||
}
|
||||
}
|
||||
/// The display name of a mob: its custom name or its type's name.
|
||||
pub(super) fn mob_display_name(&self, id: u32) -> Option<Text> {
|
||||
let m = self.mobs.iter().find(|m| m.id == id)?;
|
||||
@@ -82,8 +123,8 @@ impl Game<'_> {
|
||||
/// `LivingEntity.handleDamageEvent` for the player: the hurt sound of the
|
||||
/// source's `DamageEffects` (`Player.getHurtSound`).
|
||||
pub(super) fn player_hurt_sound(&mut self, sound: &str, at: [f64; 3]) {
|
||||
let pitch = self.audio.hurt_pitch();
|
||||
self.audio.play(sound, Some(at), 1.0, pitch);
|
||||
// Player.getVoicePitch: always 1.0.
|
||||
self.audio.play(sound, Some(at), 1.0, 1.0);
|
||||
}
|
||||
/// `CombatTracker.recordDamage` for the motion events of this tick.
|
||||
pub(super) fn record_combat(&mut self, events: &[MotionEvent]) {
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
//! tick (`execute`, `executeChain`), `BaseCommandBlock.performCommand`
|
||||
//! through the command system with the block's output, and
|
||||
//! `CommandBlockEditScreen` with what `handleSetCommandBlock` does with it.
|
||||
use super::{Game, Screen, commands::execute::Source};
|
||||
use super::{Game, Screen, commands::execute::Source, lan::LanHost};
|
||||
use horizon_client::{
|
||||
command_block::{self, CommandBlock, Mode},
|
||||
containers::BlockEntity,
|
||||
@@ -12,6 +12,7 @@ use horizon_client::{
|
||||
world::World,
|
||||
};
|
||||
use horizon_core::position::BlockPos;
|
||||
use horizon_protocol::packets::play::{self, Serverbound as S};
|
||||
use horizon_world::{
|
||||
chunk::BlockState,
|
||||
level::flags,
|
||||
@@ -35,6 +36,16 @@ pub(super) struct CommandBlockScreen {
|
||||
auto: bool,
|
||||
}
|
||||
|
||||
/// What `ServerboundSetCommandBlockPacket` carries.
|
||||
struct CommandBlockEdit {
|
||||
pos: Pos,
|
||||
command: String,
|
||||
track_output: bool,
|
||||
mode: Mode,
|
||||
conditional: bool,
|
||||
auto: bool,
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, PartialEq)]
|
||||
enum Button {
|
||||
Output,
|
||||
@@ -186,6 +197,18 @@ impl Game<'_> {
|
||||
pub(super) fn can_use_game_master_blocks(&self) -> bool {
|
||||
self.creative && self.ui.registries.permission >= 2
|
||||
}
|
||||
/// `CommandBlock.useWithoutItem` on a server's client, which opens the
|
||||
/// screen itself (`Player.openCommandBlock`) while the server only
|
||||
/// answers the click: the block at `pos`, when it is a command block the
|
||||
/// player may edit.
|
||||
pub(super) fn open_command_block_if_editable(&mut self, pos: Pos) {
|
||||
let is_command_block = self
|
||||
.block_state_at(pos)
|
||||
.is_some_and(|state| Mode::of_block(&state.name).is_some());
|
||||
if is_command_block && self.can_use_game_master_blocks() {
|
||||
self.open_command_block(pos);
|
||||
}
|
||||
}
|
||||
fn command_block(&self, pos: Pos) -> Option<&CommandBlock> {
|
||||
match self.world.block_entities.get(&pos)? {
|
||||
BlockEntity::CommandBlock(c) => Some(c),
|
||||
@@ -572,12 +595,66 @@ impl Game<'_> {
|
||||
s.command.insert_text(&allowed);
|
||||
}
|
||||
}
|
||||
/// `ServerGamePacketListenerImpl.handleSetCommandBlock`.
|
||||
/// The edit screen's Done: a server's client sends the packet, the
|
||||
/// window's own world applies it (`handleSetCommandBlock`).
|
||||
fn set_command_block(&mut self, s: &CommandBlockScreen) {
|
||||
if !self.can_use_game_master_blocks() {
|
||||
self.system_message(Text::translate("advMode.notAllowed", &[]));
|
||||
return;
|
||||
}
|
||||
let edit = CommandBlockEdit {
|
||||
pos: s.pos,
|
||||
command: s.command.value.clone(),
|
||||
track_output: s.track_output,
|
||||
mode: s.mode,
|
||||
conditional: s.conditional,
|
||||
auto: s.auto,
|
||||
};
|
||||
if self.remote.is_some() {
|
||||
self.send(S::SetCommandBlock(play::SetCommandBlock {
|
||||
pos: [edit.pos.0, edit.pos.1, edit.pos.2],
|
||||
command: edit.command,
|
||||
mode: edit.mode.ordinal(),
|
||||
flags: i8::from(edit.track_output)
|
||||
| (i8::from(edit.conditional) << 1)
|
||||
| (i8::from(edit.auto) << 2),
|
||||
}));
|
||||
} else {
|
||||
self.apply_command_block(&edit);
|
||||
}
|
||||
}
|
||||
/// `ServerGamePacketListenerImpl.handleSetCommandBlock` for a guest: the
|
||||
/// block is edited only by a creative player with game master commands.
|
||||
pub(super) fn lan_set_command_block(
|
||||
&mut self,
|
||||
host: &mut LanHost,
|
||||
conn: u32,
|
||||
p: play::SetCommandBlock,
|
||||
) {
|
||||
let Some(g) = host.hub.guest(conn) else {
|
||||
return;
|
||||
};
|
||||
let allowed = g.creative() && g.permission >= 2;
|
||||
let edit = CommandBlockEdit {
|
||||
pos: (p.pos[0], p.pos[1], p.pos[2]),
|
||||
command: p.command,
|
||||
track_output: p.flags & 1 != 0,
|
||||
mode: Mode::from_ordinal(p.mode),
|
||||
conditional: p.flags & 2 != 0,
|
||||
auto: p.flags & 4 != 0,
|
||||
};
|
||||
self.as_guest(host, conn, |game| {
|
||||
if allowed {
|
||||
game.apply_command_block(&edit);
|
||||
// BaseCommandBlock.onUpdated: the clients get the new command.
|
||||
game.mark_block_entity_updated(edit.pos);
|
||||
} else {
|
||||
game.system_message(Text::translate("advMode.notAllowed", &[]));
|
||||
}
|
||||
});
|
||||
}
|
||||
/// What `handleSetCommandBlock` does once the player may edit the block.
|
||||
fn apply_command_block(&mut self, s: &CommandBlockEdit) {
|
||||
let pos = s.pos;
|
||||
let registry = self.pack.assets.registry;
|
||||
let Some(current) = self.block_state_at(pos) else {
|
||||
@@ -609,7 +686,7 @@ impl Game<'_> {
|
||||
}
|
||||
let _ = self.process_level_events();
|
||||
}
|
||||
let command = s.command.value.clone();
|
||||
let command = s.command.clone();
|
||||
let Some(c) = command_block_mut(&mut self.world, pos) else {
|
||||
return;
|
||||
};
|
||||
|
||||
@@ -1,11 +1,10 @@
|
||||
//! `/effect` (EffectCommands) and `/enchant` (EnchantCommand).
|
||||
use super::{Arg, Context, Game, R, Target, err, texts};
|
||||
use horizon_client::{
|
||||
brewing,
|
||||
brewing::{self, namespaced},
|
||||
effects::EffectInstance,
|
||||
text::{self, Style, Text},
|
||||
};
|
||||
use std::{collections::HashMap, sync::OnceLock};
|
||||
|
||||
/// `CommandResponseTracker` over targets: elements with a non-zero result.
|
||||
#[derive(Default)]
|
||||
@@ -21,24 +20,6 @@ impl Tracker {
|
||||
}
|
||||
}
|
||||
}
|
||||
/// Namespaced effect ids for mob effect storage.
|
||||
fn namespaced(id: &'static str) -> &'static str {
|
||||
static IDS: OnceLock<HashMap<&'static str, &'static str>> = OnceLock::new();
|
||||
IDS.get_or_init(|| {
|
||||
brewing::EFFECTS
|
||||
.iter()
|
||||
.map(|e| {
|
||||
(
|
||||
e.id,
|
||||
&*Box::leak(format!("minecraft:{}", e.id).into_boxed_str()),
|
||||
)
|
||||
})
|
||||
.collect()
|
||||
})
|
||||
.get(id)
|
||||
.copied()
|
||||
.unwrap_or(id)
|
||||
}
|
||||
fn effect_name(id: &str) -> Text {
|
||||
Text::translate(&format!("effect.minecraft.{id}"), &[])
|
||||
}
|
||||
|
||||
@@ -119,6 +119,7 @@ impl Game<'_> {
|
||||
"magic" | "indirect_magic" | "dragon_breath" => DamageKind::Magic,
|
||||
"sting" => DamageKind::Sting,
|
||||
"cactus" | "sweet_berry_bush" => DamageKind::Cactus,
|
||||
"freeze" => DamageKind::Freeze,
|
||||
"in_wall" => DamageKind::InWall,
|
||||
"thorns" => DamageKind::Thorns,
|
||||
_ => DamageKind::Generic,
|
||||
|
||||
@@ -193,6 +193,8 @@ impl Game<'_> {
|
||||
last_hurt: self.combat.last_hurt_mob.map(|(id, at)| (Who::Mob(id), at)),
|
||||
velocity: p.velocity,
|
||||
yaw: self.yaw,
|
||||
sprinting: p.sprinting,
|
||||
riding: self.riding.is_some(),
|
||||
hero: self.potions.effects.get("hero_of_the_village").is_some(),
|
||||
effects: self.potions.effects.list.iter().map(|e| e.id).collect(),
|
||||
uuid: self.level.player_uuid.unwrap_or_default(),
|
||||
@@ -402,6 +404,7 @@ impl Game<'_> {
|
||||
world: &self.world,
|
||||
registry: self.pack.assets.registry,
|
||||
facts: self.res.facts(),
|
||||
fluid_tables: &self.motion.fluids,
|
||||
movement: &self.motion.blocks,
|
||||
collision: self.collision,
|
||||
data: self.res.mob_data(),
|
||||
@@ -679,6 +682,7 @@ impl Game<'_> {
|
||||
self.sync_riders();
|
||||
self.tick_skeleton_traps();
|
||||
self.tick_apparition();
|
||||
self.guardian_bubbles();
|
||||
self.villager_reputation_tick();
|
||||
self.merchant_tick();
|
||||
self.villager_pickups();
|
||||
@@ -690,6 +694,7 @@ impl Game<'_> {
|
||||
self.tick_projectiles()?;
|
||||
self.tick_fangs();
|
||||
self.tick_allays();
|
||||
self.tick_pandas();
|
||||
self.tick_breeze_effects();
|
||||
self.tick_sniffer_effects();
|
||||
self.piglin_pickups();
|
||||
@@ -933,6 +938,18 @@ impl Game<'_> {
|
||||
match e {
|
||||
MobEvent::SquidCycle { mob, at } => self.lan_entity_event(mob as i32, 19, at),
|
||||
MobEvent::RabbitHop { mob, at } => self.lan_entity_event(mob as i32, 1, at),
|
||||
MobEvent::Drown { mob, at } => {
|
||||
self.lan_entity_event(mob as i32, super::combat::DROWN_EVENT, at);
|
||||
let v = self
|
||||
.mobs
|
||||
.iter()
|
||||
.find(|m| m.id == mob)
|
||||
.map_or([0.0; 3], |m| [m.velocity.x, m.velocity.y, m.velocity.z]);
|
||||
self.drown_particles(at, v);
|
||||
}
|
||||
MobEvent::EntityEvent { mob, event, at } => {
|
||||
self.lan_entity_event(mob as i32, event, at)
|
||||
}
|
||||
MobEvent::Sound(name, at, volume, pitch)
|
||||
| MobEvent::LocalSound(name, at, volume, pitch) => {
|
||||
self.audio
|
||||
@@ -980,6 +997,19 @@ impl Game<'_> {
|
||||
}
|
||||
}
|
||||
}
|
||||
MobEvent::Knockback {
|
||||
target,
|
||||
power,
|
||||
x,
|
||||
z,
|
||||
} => match target {
|
||||
Who::Player => self.knock_back_player(power, x, z),
|
||||
Who::Mob(id) => {
|
||||
if let Some(m) = self.mobs.iter_mut().find(|m| m.id == id) {
|
||||
m.knockback(power, x, z);
|
||||
}
|
||||
}
|
||||
},
|
||||
MobEvent::Wololo { mob } => {
|
||||
if let Some(m) = self.mobs.iter_mut().find(|m| m.id == mob) {
|
||||
// DyeColor.RED.
|
||||
@@ -1372,9 +1402,14 @@ impl Game<'_> {
|
||||
m.ai.aggressive |= aggressive;
|
||||
}
|
||||
}
|
||||
MobEvent::PandaSneezed { mob } => self.panda_sneezed(mob),
|
||||
MobEvent::GiftLoot { mob, table, at } => {
|
||||
// Turtle.ageBoundaryReached: only with mob loot on.
|
||||
if table == "minecraft:gameplay/turtle_grow" && !self.ui.rules.bool("mob_drops")
|
||||
// Turtle.ageBoundaryReached and Panda.afterSneeze: only
|
||||
// with mob loot on.
|
||||
if matches!(
|
||||
table,
|
||||
"minecraft:gameplay/turtle_grow" | "minecraft:gameplay/panda_sneeze"
|
||||
) && !self.ui.rules.bool("mob_drops")
|
||||
{
|
||||
continue;
|
||||
}
|
||||
@@ -1611,6 +1646,13 @@ impl Game<'_> {
|
||||
self.spawn_breeding_orb([p.position.x, p.position.y, p.position.z], xp);
|
||||
return;
|
||||
}
|
||||
// Panda.getBreedOffspring: genes from both parents.
|
||||
if p.species() == Species::Panda {
|
||||
horizon_client::mobs::panda::inherit(child, &p, q.as_ref().unwrap_or(&p));
|
||||
let xp = 1 + self.world.random.next_int_bound(7).unwrap_or(0) as u32;
|
||||
self.spawn_breeding_orb([p.position.x, p.position.y, p.position.z], xp);
|
||||
return;
|
||||
}
|
||||
// Nautilus.getBreedOffspring: a tame parent's baby is tame too.
|
||||
if p.species() == Species::Nautilus && p.extra.tame {
|
||||
child.extra.tame = true;
|
||||
@@ -1619,6 +1661,10 @@ impl Game<'_> {
|
||||
}
|
||||
// Offspring inherit a parent's variant (pigs/cows/chickens) or mixed wool.
|
||||
child.extra.variant = p.extra.variant;
|
||||
// Fox.getBreedOffspring: either parent's variant.
|
||||
if p.species() == Species::Fox && !coin {
|
||||
child.extra.variant = q.as_ref().map_or(p.extra.variant, |q| q.extra.variant);
|
||||
}
|
||||
if p.species() == Species::Sheep {
|
||||
child.extra.color = p.extra.color;
|
||||
}
|
||||
@@ -1920,6 +1966,7 @@ impl Game<'_> {
|
||||
let mut hits = Vec::new();
|
||||
let mut deflections: Vec<[f64; 3]> = Vec::new();
|
||||
let mut arrow_events = Vec::new();
|
||||
let mut trails = Vec::new();
|
||||
{
|
||||
let mut targets: Vec<(Who, Aabb)> = self
|
||||
.mobs
|
||||
@@ -2011,6 +2058,25 @@ impl Game<'_> {
|
||||
if shooter.is_some() {
|
||||
p.owner = Some(Who::Player);
|
||||
}
|
||||
// AbstractHurtingProjectile.tick ends with createParticleTrail.
|
||||
if super::remote_projectiles::has_smoke_trail(&p.kind) {
|
||||
trails.push(p.position);
|
||||
}
|
||||
// ThrownEnderpearl.onAboveBubbleColumn / onInsideBubbleColumn.
|
||||
if keep && hit == Hit::None && matches!(p.kind, ProjectileKind::EnderPearl) {
|
||||
let (at, h) = (p.position, 0.125);
|
||||
horizon_client::motion::bubble_column_push(
|
||||
w.world,
|
||||
w.registry,
|
||||
w.facts,
|
||||
w.collision,
|
||||
Aabb::new(
|
||||
Vec3::new(at.x - h, at.y, at.z - h),
|
||||
Vec3::new(at.x + h, at.y + 2.0 * h, at.z + h),
|
||||
),
|
||||
&mut p.velocity,
|
||||
);
|
||||
}
|
||||
if let Some(a) = p.arrow.as_mut() {
|
||||
arrow_events.extend(a.events.drain(..).map(|e| (p.position, e)));
|
||||
}
|
||||
@@ -2053,6 +2119,7 @@ impl Game<'_> {
|
||||
keep
|
||||
});
|
||||
}
|
||||
self.smoke_trails(trails);
|
||||
for at in deflections {
|
||||
self.audio
|
||||
.play("minecraft:entity.breeze.deflect", Some(at), 1.0, 1.0);
|
||||
@@ -2217,11 +2284,9 @@ impl Game<'_> {
|
||||
ArrowEvent::ShootEvent | ArrowEvent::Drop => {}
|
||||
}
|
||||
}
|
||||
/// `Level.isRainingAt` for arrows: raining, open to the sky (the biome's
|
||||
/// precipitation is not checked here).
|
||||
/// `Level.isRainingAt` for arrows.
|
||||
fn arrow_rain(&self) -> impl Fn(BlockPos) -> bool + '_ {
|
||||
let raining = self.world.weather.levels(1.0).0 > 0.2;
|
||||
move |p: BlockPos| raining && self.world.light(p.x, p.y, p.z) >> 4 == 15
|
||||
move |p: BlockPos| self.raining_at(p)
|
||||
}
|
||||
/// The level as arrows see it.
|
||||
fn arrow_scene<'s>(
|
||||
@@ -2834,6 +2899,14 @@ impl Game<'_> {
|
||||
let creative = self.creative;
|
||||
// The hand's slot (Player.interactOn passes the hand).
|
||||
let selected = self.held_slot();
|
||||
// Panda.mobInteract: bamboo, or a click on a panda lying on its back.
|
||||
if self.mobs[index].species() == Species::Panda && self.mobs[index].alive() {
|
||||
let taken = self.interact_panda(index, &item);
|
||||
if taken {
|
||||
self.swing_arm();
|
||||
}
|
||||
return Ok(taken);
|
||||
}
|
||||
if self.interact_zombie_villager(index, &item) {
|
||||
return Ok(true);
|
||||
}
|
||||
@@ -2886,6 +2959,7 @@ impl Game<'_> {
|
||||
.items()
|
||||
.get(&item)
|
||||
.is_some_and(|i| i.max_damage > 0);
|
||||
let acting = self.acting_player_uuid();
|
||||
let m = &mut self.mobs[index];
|
||||
if !m.alive() {
|
||||
return Ok(false);
|
||||
@@ -2980,6 +3054,7 @@ impl Game<'_> {
|
||||
}
|
||||
Species::Wolf
|
||||
if m.extra.tame
|
||||
&& m.extra.owner == Some(acting)
|
||||
&& item.ends_with("_dye")
|
||||
&& horizon_client::mobs::render::DYE_NAMES
|
||||
.iter()
|
||||
@@ -3002,7 +3077,9 @@ impl Game<'_> {
|
||||
consume = true;
|
||||
}
|
||||
Species::Wolf
|
||||
if m.extra.tame && !(is_food && m.extra.age == 0 && m.extra.in_love <= 0) =>
|
||||
if m.extra.tame
|
||||
&& m.extra.owner == Some(acting)
|
||||
&& !(is_food && m.extra.age == 0 && m.extra.in_love <= 0) =>
|
||||
{
|
||||
m.extra.sitting = !m.extra.sitting;
|
||||
m.ai.jumping = false;
|
||||
@@ -3022,7 +3099,7 @@ impl Game<'_> {
|
||||
let tamed = m.random.next_int_bound(3).unwrap_or(1) == 0;
|
||||
if tamed {
|
||||
horizon_client::mobs::tame::set_tame(m, true);
|
||||
m.extra.owner = self.level.player_uuid.or(Some([0, 0, 0, 1]));
|
||||
m.extra.owner = Some(acting);
|
||||
m.persistent = true;
|
||||
m.nav_stop();
|
||||
m.ai.target = None;
|
||||
@@ -3049,6 +3126,7 @@ impl Game<'_> {
|
||||
// Cat.mobInteract: dye, heal or sit when tame; fish may tame a wild one.
|
||||
Species::Cat
|
||||
if m.extra.tame
|
||||
&& m.extra.owner == Some(acting)
|
||||
&& item.ends_with("_dye")
|
||||
&& horizon_client::mobs::render::DYE_NAMES
|
||||
.iter()
|
||||
@@ -3071,7 +3149,12 @@ impl Game<'_> {
|
||||
m.persistent = true;
|
||||
consume = true;
|
||||
}
|
||||
Species::Cat if m.extra.tame && is_food && m.health < m.attrs.max_health => {
|
||||
Species::Cat
|
||||
if m.extra.tame
|
||||
&& m.extra.owner == Some(acting)
|
||||
&& is_food
|
||||
&& m.health < m.attrs.max_health =>
|
||||
{
|
||||
let heal = self
|
||||
.res
|
||||
.items()
|
||||
@@ -3084,7 +3167,9 @@ impl Game<'_> {
|
||||
eat = eating_sound(m, &mut self.world.random);
|
||||
}
|
||||
Species::Cat
|
||||
if m.extra.tame && !(is_food && m.extra.age == 0 && m.extra.in_love <= 0) =>
|
||||
if m.extra.tame
|
||||
&& m.extra.owner == Some(acting)
|
||||
&& !(is_food && m.extra.age == 0 && m.extra.in_love <= 0) =>
|
||||
{
|
||||
m.extra.sitting = !m.extra.sitting;
|
||||
self.world.modified.insert((
|
||||
@@ -3098,7 +3183,7 @@ impl Game<'_> {
|
||||
let tamed = m.random.next_int_bound(3).unwrap_or(1) == 0;
|
||||
if tamed {
|
||||
m.extra.tame = true;
|
||||
m.extra.owner = self.level.player_uuid.or(Some([0, 0, 0, 1]));
|
||||
m.extra.owner = Some(acting);
|
||||
m.extra.sitting = true;
|
||||
}
|
||||
eat = eating_sound(m, &mut self.world.random);
|
||||
@@ -3402,6 +3487,60 @@ impl Game<'_> {
|
||||
}
|
||||
}
|
||||
}
|
||||
/// `Guardian.aiStep` on a client: two bubbles a tick trailing a guardian
|
||||
/// that swims along, and bubbles up the beam to its target. The server
|
||||
/// never draws them, so the mob's own random stays untouched.
|
||||
pub(super) fn guardian_bubbles(&mut self) {
|
||||
use horizon_client::mobs::guardian;
|
||||
use horizon_client::particles::{ParticleOptions, ParticleType};
|
||||
let mut swimming = Vec::new();
|
||||
let mut beams = Vec::new();
|
||||
for m in &self.mobs {
|
||||
if !m.alive() || !matches!(m.species(), Species::Guardian | Species::ElderGuardian) {
|
||||
continue;
|
||||
}
|
||||
let at = [m.position.x, m.position.y, m.position.z];
|
||||
if m.extra.flag2 && m.fluids.in_water {
|
||||
let (width, height, _) = m.dims();
|
||||
swimming.push((at, width, height, m.view_vector()));
|
||||
}
|
||||
if let Some((_, scale)) = guardian::beam(m, 0.0)
|
||||
&& let Some(t) = self.beam_target(m.ai.target, 1.0)
|
||||
{
|
||||
beams.push(([at[0], m.eye_y(), at[2]], [t.x, t.y, t.z], f64::from(scale)));
|
||||
}
|
||||
}
|
||||
let bubble = ParticleOptions::simple(ParticleType::Bubble);
|
||||
for (at, width, height, view) in swimming {
|
||||
for _ in 0..2 {
|
||||
let (x, y, z) = {
|
||||
let r = self.particles.random();
|
||||
(r.next_double(), r.next_double(), r.next_double())
|
||||
};
|
||||
let p = [
|
||||
at[0] + width * (2.0 * x - 1.0) * 0.5 - view[0] * 1.5,
|
||||
at[1] + height * y - view[1] * 1.5,
|
||||
at[2] + width * (2.0 * z - 1.0) * 0.5 - view[2] * 1.5,
|
||||
];
|
||||
self.particles.add(&bubble, p, [0.0; 3]);
|
||||
}
|
||||
}
|
||||
for (eye, target, scale) in beams {
|
||||
let d = [target[0] - eye[0], target[1] - eye[1], target[2] - eye[2]];
|
||||
let length = (d[0] * d[0] + d[1] * d[1] + d[2] * d[2]).sqrt();
|
||||
if length <= 0.0 {
|
||||
continue;
|
||||
}
|
||||
let mut dist = self.particles.random().next_double();
|
||||
while dist < length {
|
||||
let r = self.particles.random();
|
||||
dist += 1.8 - scale + r.next_double() * (1.7 - scale);
|
||||
let f = dist / length;
|
||||
let p = [eye[0] + d[0] * f, eye[1] + d[1] * f, eye[2] + d[2] * f];
|
||||
self.particles.add(&bubble, p, [0.0; 3]);
|
||||
}
|
||||
}
|
||||
}
|
||||
/// Mob models and projectiles for the entity pass.
|
||||
pub(super) fn mob_geometry(&self, camera: glam::DVec3, alpha: f64, out: &mut Vec<Vertex>) {
|
||||
let atlas = &self.gui.atlas;
|
||||
@@ -3409,8 +3548,10 @@ impl Game<'_> {
|
||||
let scale = f64::from(self.settings.entity_distance);
|
||||
for mob in &self.mobs {
|
||||
let entity_start = out.len();
|
||||
// Entity.DATA_SHARED_FLAGS_ID invisible: only the name tag draws.
|
||||
if mob.invisible {
|
||||
let outline = self.mob_outline(mob);
|
||||
// Entity.DATA_SHARED_FLAGS_ID invisible: only the name tag draws (and
|
||||
// the outline of a glowing mob, LivingEntityRenderer.getRenderType).
|
||||
if mob.invisible && outline.is_none() {
|
||||
continue;
|
||||
}
|
||||
let d = glam::DVec3::new(mob.position.x, mob.position.y, mob.position.z) - camera;
|
||||
@@ -3472,9 +3613,18 @@ impl Game<'_> {
|
||||
out,
|
||||
);
|
||||
self.mob_block_geometry(mob, camera, alpha, packed, out);
|
||||
if let Some(rgb) = outline {
|
||||
self.mark_outline(&out[entity_start..], rgb);
|
||||
if mob.invisible {
|
||||
out.truncate(entity_start);
|
||||
continue;
|
||||
}
|
||||
}
|
||||
// Glowing outlines and a guardian's beam remain visible independently
|
||||
// of whether the body is hidden behind terrain.
|
||||
if !mob.glowing && horizon_client::mobs::guardian::beam(mob, alpha as f32).is_none() {
|
||||
if outline.is_none()
|
||||
&& horizon_client::mobs::guardian::beam(mob, alpha as f32).is_none()
|
||||
{
|
||||
self.cull_entity_vertices(out, entity_start);
|
||||
}
|
||||
// GuardianRenderer: the laser to the locked target.
|
||||
@@ -3522,7 +3672,15 @@ impl Game<'_> {
|
||||
super::pickups::Taken::Arrow(a) => Some((&**a, super::pickups::position(p, alpha))),
|
||||
_ => None,
|
||||
});
|
||||
// A glowing projectile's outline: what it drew up to the next one.
|
||||
let mut glowing = None;
|
||||
for (a, at) in live.chain(taken) {
|
||||
if let Some((start, rgb)) = glowing.take() {
|
||||
self.mark_outline(&out[start..], rgb);
|
||||
}
|
||||
if a.id != 0 && self.entity_glowing(a.id) {
|
||||
glowing = Some((out.len(), super::glowing::team_color(self.mob_team(a.id))));
|
||||
}
|
||||
let p = glam::DVec3::from_array(at) - camera;
|
||||
let c = p.as_vec3();
|
||||
let light = self.world.light(
|
||||
@@ -3654,6 +3812,9 @@ impl Game<'_> {
|
||||
});
|
||||
}
|
||||
}
|
||||
if let Some((start, rgb)) = glowing {
|
||||
self.mark_outline(&out[start..], rgb);
|
||||
}
|
||||
}
|
||||
}
|
||||
/// `Bucketable.getBucketItemStack` and `getPickupSound` of a mob that a water
|
||||
@@ -3703,6 +3864,7 @@ pub(super) fn damage_type(kind: horizon_client::mobs::DamageKind, player: bool)
|
||||
K::InWall => "minecraft:in_wall",
|
||||
K::Thorns => "minecraft:thorns",
|
||||
K::SonicBoom => "minecraft:sonic_boom",
|
||||
K::Freeze => "minecraft:freeze",
|
||||
K::OutOfWorld => "minecraft:out_of_world",
|
||||
}
|
||||
}
|
||||
|
||||
@@ -13,7 +13,7 @@ use horizon_client::{
|
||||
save::VERSION_NAME,
|
||||
text::Text,
|
||||
};
|
||||
use horizon_core::geometry::Axis;
|
||||
use horizon_core::geometry::{Axis, Vec3};
|
||||
use winit::keyboard::KeyCode;
|
||||
|
||||
/// `ChatFormatting.YELLOW` and `RED`.
|
||||
@@ -766,7 +766,8 @@ impl Game<'_> {
|
||||
}
|
||||
let lerp = |a: f64, b: f64| a + f64::from(alpha) * (b - a);
|
||||
let lerpf = |a: f32, b: f32| a + alpha * (b - a);
|
||||
for m in &self.mobs {
|
||||
let guests = self.lan.iter().flat_map(|h| h.models.values());
|
||||
for m in self.mobs.iter().chain(guests) {
|
||||
if m.removed || m.invisible || m.has_effect("minecraft:invisibility") {
|
||||
continue;
|
||||
}
|
||||
@@ -796,6 +797,159 @@ impl Game<'_> {
|
||||
// EntityType.ITEM: 0.25 by 0.25, eyes at 0.2125.
|
||||
g.hitbox(pos, (0.25, 0.25), 0.2125, false, (0.0, 0.0));
|
||||
}
|
||||
// The other entities (EntityTypes sizes; eyes at 0.85 of the height
|
||||
// unless the type sets one). `at` is the interpolated position.
|
||||
let at = |previous: Vec3, now: Vec3| {
|
||||
[
|
||||
lerp(previous.x, now.x),
|
||||
lerp(previous.y, now.y),
|
||||
lerp(previous.z, now.z),
|
||||
]
|
||||
};
|
||||
for p in &self.paintings {
|
||||
let (min, max) = p.bounds();
|
||||
let centre = std::array::from_fn(|i| (min[i] + max[i]) / 2.0);
|
||||
g.entity_box(min, max, centre, 0.425, false, step_view(p.step()));
|
||||
}
|
||||
for f in self.item_frames.iter().filter(|f| !f.invisible) {
|
||||
let (min, max) = f.bounds();
|
||||
let centre = std::array::from_fn(|i| (min[i] + max[i]) / 2.0);
|
||||
g.entity_box(min, max, centre, 0.0, false, step_view(f.step()));
|
||||
}
|
||||
for c in &self.cushions {
|
||||
let (min, max) = c.bounds();
|
||||
let p = c.position;
|
||||
g.entity_box(min, max, [p.x, p.y, p.z], 0.2125, false, (0.0, c.yaw));
|
||||
}
|
||||
for a in self.armor_stands.iter().filter(|a| !a.invisible) {
|
||||
// ArmorStand.getDimensions: a marker has none, a small one is
|
||||
// half scale with its eyes at 0.9875.
|
||||
let (size, eye) = match (a.marker, a.small) {
|
||||
(true, _) => ((0.0, 0.0), 0.0),
|
||||
(_, true) => ((0.25, 0.9875), 0.9875),
|
||||
_ => ((0.5, 1.975), 1.7775),
|
||||
};
|
||||
let p = a.position;
|
||||
g.hitbox([p.x, p.y, p.z], size, eye, true, (0.0, a.yaw));
|
||||
}
|
||||
for b in &self.boats {
|
||||
let h = super::boats::HEIGHT as f32;
|
||||
g.hitbox(
|
||||
at(b.previous, b.position),
|
||||
(super::boats::WIDTH as f32, h),
|
||||
h,
|
||||
false,
|
||||
(0.0, b.yaw),
|
||||
);
|
||||
}
|
||||
for c in &self.carts {
|
||||
let (w, h) = (
|
||||
super::minecarts::WIDTH as f32,
|
||||
super::minecarts::HEIGHT as f32,
|
||||
);
|
||||
g.hitbox(
|
||||
at(c.previous, c.position),
|
||||
(w, h),
|
||||
h * 0.85,
|
||||
false,
|
||||
(0.0, c.yaw),
|
||||
);
|
||||
}
|
||||
for o in &self.orbs {
|
||||
g.hitbox(
|
||||
at(o.previous, o.position),
|
||||
(0.5, 0.5),
|
||||
0.425,
|
||||
false,
|
||||
(0.0, 0.0),
|
||||
);
|
||||
}
|
||||
for f in &self.falling {
|
||||
g.hitbox(
|
||||
at(f.previous, f.position),
|
||||
(0.98, 0.98),
|
||||
0.833,
|
||||
false,
|
||||
(0.0, 0.0),
|
||||
);
|
||||
}
|
||||
for t in &self.tnts {
|
||||
g.hitbox(
|
||||
at(t.previous, t.position),
|
||||
(0.98, 0.98),
|
||||
0.15,
|
||||
false,
|
||||
(0.0, 0.0),
|
||||
);
|
||||
}
|
||||
for c in &self.crystals {
|
||||
let p = c.position;
|
||||
g.hitbox([p.x, p.y, p.z], (2.0, 2.0), 1.7, false, (0.0, 0.0));
|
||||
}
|
||||
for p in &self.creatures.projectiles {
|
||||
use horizon_client::projectiles::ProjectileKind as K;
|
||||
let (size, eye) = match p.kind {
|
||||
K::Arrow { .. } | K::Trident => (0.5, 0.13),
|
||||
K::Snowball | K::Egg | K::EnderPearl | K::LlamaSpit | K::Potion { .. } => {
|
||||
(0.25, 0.2125)
|
||||
}
|
||||
K::SmallFireball | K::ShulkerBullet { .. } | K::WitherSkull { .. } => {
|
||||
(0.3125, 0.265_625)
|
||||
}
|
||||
K::WindCharge | K::BreezeWindCharge => (0.3125, 0.0),
|
||||
K::LargeFireball { .. } => (1.0, 0.85),
|
||||
};
|
||||
g.hitbox(
|
||||
at(p.previous, p.position),
|
||||
(size, size),
|
||||
eye,
|
||||
false,
|
||||
(p.pitch, p.yaw),
|
||||
);
|
||||
}
|
||||
for t in &self.tridents {
|
||||
g.hitbox(
|
||||
at(t.previous, t.position),
|
||||
(0.5, 0.5),
|
||||
0.13,
|
||||
false,
|
||||
(t.pitch, t.yaw),
|
||||
);
|
||||
}
|
||||
for r in &self.rockets {
|
||||
// Projectile.updateRotation: the look follows the motion.
|
||||
let v = r.velocity;
|
||||
let yaw = v.x.atan2(v.z).to_degrees() as f32;
|
||||
let pitch = v.y.atan2(v.x.hypot(v.z)).to_degrees() as f32;
|
||||
g.hitbox(
|
||||
at(r.previous, r.position),
|
||||
(0.25, 0.25),
|
||||
0.2125,
|
||||
false,
|
||||
(pitch, yaw),
|
||||
);
|
||||
}
|
||||
for h in self
|
||||
.hook
|
||||
.iter()
|
||||
.map(|h| (h.previous, h.position))
|
||||
.chain(self.remote_hooks.iter().map(|h| (h.previous, h.position)))
|
||||
{
|
||||
g.hitbox(at(h.0, h.1), (0.25, 0.25), 0.2125, false, (0.0, 0.0));
|
||||
}
|
||||
for b in &self.bolts {
|
||||
g.hitbox(b.at, (0.0, 0.0), 0.0, false, (0.0, 0.0));
|
||||
}
|
||||
// The dragon: 16 by 8, a living entity; its parts follow.
|
||||
if let Some(d) = &self.dragon {
|
||||
g.hitbox(
|
||||
at(d.previous, d.position),
|
||||
(16.0, 8.0),
|
||||
6.8,
|
||||
true,
|
||||
(0.0, d.yaw),
|
||||
);
|
||||
}
|
||||
// The dragon's parts.
|
||||
for (id, b) in self.special_targets() {
|
||||
if (super::dragon::SPECIAL..super::dragon::SPECIAL + 8).contains(&id) {
|
||||
@@ -828,3 +982,12 @@ impl Game<'_> {
|
||||
g
|
||||
}
|
||||
}
|
||||
|
||||
/// The (x, y) rotations whose view vector is the step of a direction.
|
||||
fn step_view(step: [i32; 3]) -> (f32, f32) {
|
||||
let [x, y, z] = step.map(f64::from);
|
||||
(
|
||||
(-y).asin().to_degrees() as f32,
|
||||
(-x.atan2(z)).to_degrees() as f32,
|
||||
)
|
||||
}
|
||||
@@ -2127,10 +2127,12 @@ impl Game<'_> {
|
||||
c.at[1],
|
||||
c.at[2] + f64::from(a.sin() * d),
|
||||
];
|
||||
self.particles.add(
|
||||
self.particles.add_with(
|
||||
&ParticleOptions::simple(ParticleType::DragonBreath),
|
||||
at,
|
||||
[0.0, 0.0, 0.0],
|
||||
false,
|
||||
true,
|
||||
);
|
||||
}
|
||||
// AreaEffectCloud: after its 20-tick wait, every 20 ticks per entity.
|
||||
|
||||
@@ -253,10 +253,17 @@ impl Game<'_> {
|
||||
out
|
||||
}
|
||||
|
||||
/// LightmapRenderStateExtractor: End flashes add to the sky light factor.
|
||||
/// LightmapRenderStateExtractor: End flashes add to the sky light factor
|
||||
/// unless Hide Lightning Flashes is on (the sky sprite is not gated), a
|
||||
/// third of it under a boss bar's world fog.
|
||||
pub(super) fn end_flash_sky(&self, sky: &mut horizon_client::environment::Sky, partial: f64) {
|
||||
if self.dim == Dim::End {
|
||||
sky.sky_light_factor += self.end.flash.intensity(partial as f32);
|
||||
if self.dim == Dim::End && !self.settings.file.bool("hideLightningFlashes") {
|
||||
let intensity = self.end.flash.intensity(partial as f32);
|
||||
sky.sky_light_factor += if self.boss_overlay.should_create_world_fog() {
|
||||
intensity / 3.0
|
||||
} else {
|
||||
intensity
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@@ -599,7 +606,9 @@ impl Game<'_> {
|
||||
|
||||
/// EnderDragonFight without a dragon: once the centre of the main island
|
||||
/// is loaded, place the active exit portal (EndPodiumFeature) and the
|
||||
/// first gateway (spawnNewGateway) if the portal is not there yet.
|
||||
/// first gateway (spawnNewGateway) if the portal is not there yet. Like
|
||||
/// scanState and spawnExitPortal, only while the fight has no exit portal
|
||||
/// location: a podium that is later broken stays broken.
|
||||
fn ensure_exit_portal(&mut self) -> Result<()> {
|
||||
if self.travelling.is_some()
|
||||
|| !(-1..=0).all(|cz| (-1..=0).all(|cx| self.streaming.is_lit(cx, cz)))
|
||||
@@ -609,8 +618,14 @@ impl Game<'_> {
|
||||
}
|
||||
let won = self.fight.previously_killed;
|
||||
let gateway = self.first_gateway();
|
||||
if (0..256).any(|y| &*self.block_name([0, y, 0]) == "minecraft:bedrock") {
|
||||
// The podium exists; the gateway may have been out of range when it was placed.
|
||||
if self.exit_portal().is_none() {
|
||||
// findExitPortal remembers the location of a podium that is already there.
|
||||
if let Some(level) = self.podium_level() {
|
||||
self.set_exit_portal([0, level, 0]);
|
||||
}
|
||||
}
|
||||
if self.exit_portal().is_some() {
|
||||
// The gateway may have been out of range when it was placed.
|
||||
if won
|
||||
&& self.world.loaded(gateway[0], gateway[2])
|
||||
&& &*self.block_name(gateway) != "minecraft:end_gateway"
|
||||
@@ -641,17 +656,40 @@ impl Game<'_> {
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
/// The portal level of the podium around the centre column, read from the
|
||||
/// bedrock that EndPodiumFeature places (rim at the portal level, disc
|
||||
/// below it, the torch column) so a broken column does not hide it.
|
||||
fn podium_level(&self) -> Option<i32> {
|
||||
let mut offsets: Vec<[i32; 3]> = (0..4).map(|dy| [0, dy, 0]).collect();
|
||||
for z in -4..=4i32 {
|
||||
for x in -4..=4i32 {
|
||||
let flat = f64::from(x * x + z * z);
|
||||
if (6.25..12.25).contains(&flat) {
|
||||
offsets.push([x, 0, z]);
|
||||
} else if flat + 1.0 < 6.25 {
|
||||
offsets.push([x, -1, z]);
|
||||
}
|
||||
}
|
||||
}
|
||||
(1..253)
|
||||
.map(|y| {
|
||||
let score = offsets
|
||||
.iter()
|
||||
.filter(|o| &*self.block_name([o[0], y + o[1], o[2]]) == "minecraft:bedrock")
|
||||
.count();
|
||||
(score, y)
|
||||
})
|
||||
.filter(|&(score, _)| score >= 3)
|
||||
.max()
|
||||
.map(|(_, y)| y)
|
||||
}
|
||||
/// `spawnExitPortal(true)` after the dragon dies.
|
||||
pub(super) fn activate_exit_portal(&mut self) -> Result<()> {
|
||||
let origin = match self.exit_portal() {
|
||||
Some(o) => o,
|
||||
None => {
|
||||
// The rim's level: the top of the bedrock column minus three.
|
||||
let top = (0..256)
|
||||
.rev()
|
||||
.find(|&y| &*self.block_name([0, y, 0]) == "minecraft:bedrock")
|
||||
.unwrap_or(64);
|
||||
[0, top - 3, 0]
|
||||
// The rim's level, found without assuming the centre column is intact.
|
||||
[0, self.podium_level().unwrap_or(61), 0]
|
||||
}
|
||||
};
|
||||
self.place_podium(origin, true)
|
||||
|
||||
@@ -23,7 +23,7 @@ pub(super) struct Rocket {
|
||||
/// The entity id a server tracks it by (0 until it is tracked).
|
||||
pub id: u32,
|
||||
pub(super) position: Vec3,
|
||||
previous: Vec3,
|
||||
pub(super) previous: Vec3,
|
||||
pub(super) velocity: Vec3,
|
||||
life: i32,
|
||||
lifetime: i32,
|
||||
|
||||
@@ -30,8 +30,8 @@ enum State {
|
||||
pub(super) struct Hook {
|
||||
/// Its entity id, which a server's players track it under.
|
||||
id: u32,
|
||||
position: Vec3,
|
||||
previous: Vec3,
|
||||
pub(super) position: Vec3,
|
||||
pub(super) previous: Vec3,
|
||||
velocity: Vec3,
|
||||
state: State,
|
||||
life: i32,
|
||||
@@ -770,8 +770,8 @@ impl Game<'_> {
|
||||
pub(super) struct RemoteHook {
|
||||
id: u32,
|
||||
owner: u32,
|
||||
position: Vec3,
|
||||
previous: Vec3,
|
||||
pub(super) position: Vec3,
|
||||
pub(super) previous: Vec3,
|
||||
}
|
||||
|
||||
impl Hook {
|
||||
|
||||
@@ -456,7 +456,7 @@ impl Game<'_> {
|
||||
}
|
||||
for (id, m, light) in maps {
|
||||
let entity_start = out.len();
|
||||
self.map_vertices(id, m, light, out);
|
||||
self.map_vertices(id, true, m, light, out);
|
||||
self.cull_entity_vertices(out, entity_start);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,53 @@
|
||||
//! Glowing entities (`EntityRenderer.extractRenderState`'s outlineColor): which entities
|
||||
//! appear glowing, in which team colour, and their silhouettes for the renderer's entity
|
||||
//! outline (`OutlineBufferSource`).
|
||||
use super::{Game, Vertex};
|
||||
use horizon_client::{mobs::Mob, scoreboard as sb};
|
||||
|
||||
/// `Entity.getTeamColor`: the team's colour, else white.
|
||||
pub(super) fn team_color(team: Option<&sb::Team>) -> u32 {
|
||||
team.and_then(|t| t.color.as_deref())
|
||||
.and_then(sb::team_color_rgb)
|
||||
.unwrap_or(0xFF_FFFF)
|
||||
}
|
||||
|
||||
impl Game<'_> {
|
||||
/// `Entity.isCurrentlyGlowing` of an entity that is not living (an item, a projectile):
|
||||
/// its `Glowing` tag, or shared flag 6 as a server sent it.
|
||||
pub(super) fn entity_glowing(&self, id: u32) -> bool {
|
||||
self.remote_glowing(id).unwrap_or_else(|| {
|
||||
self.persist_raw(id)
|
||||
.and_then(|t| horizon_client::save::number(t.get("Glowing")))
|
||||
.is_some_and(|b| b != 0)
|
||||
})
|
||||
}
|
||||
|
||||
/// `Minecraft.shouldEntityAppearGlowing` of a mob or another player, with its outline
|
||||
/// colour (`getTeamColor`; another player's team goes by name).
|
||||
pub(super) fn mob_outline(&self, m: &Mob) -> Option<u32> {
|
||||
if !m.is_currently_glowing() {
|
||||
return None;
|
||||
}
|
||||
let team = if &*m.kind.id == "minecraft:player" {
|
||||
let names = self.remote_player_names();
|
||||
let name = names.into_iter().find(|(id, _)| *id == m.id);
|
||||
name.and_then(|(_, name)| self.score.board.players_team(&name))
|
||||
} else {
|
||||
self.mob_team(m.id)
|
||||
};
|
||||
Some(team_color(team))
|
||||
}
|
||||
|
||||
/// Adds one glowing entity's geometry to the outline pass in `rgb`, the textures'
|
||||
/// alpha kept (`OutlineBufferSource`'s team-coloured copy of each draw).
|
||||
pub(super) fn mark_outline(&self, vertices: &[Vertex], rgb: u32) {
|
||||
let color = horizon_client::mobs::render::abgr(rgb);
|
||||
self.glowing_vertices
|
||||
.borrow_mut()
|
||||
.extend(vertices.iter().map(|v| Vertex {
|
||||
color,
|
||||
normal: [0; 4],
|
||||
..*v
|
||||
}));
|
||||
}
|
||||
}
|
||||
@@ -405,7 +405,12 @@ impl Game<'_> {
|
||||
) {
|
||||
self.submit_item_world(&state, copy, vertex_light(packed), out);
|
||||
}
|
||||
self.cull_entity_vertices(out, entity_start);
|
||||
if self.entity_glowing(item.id) {
|
||||
let rgb = super::glowing::team_color(self.mob_team(item.id));
|
||||
self.mark_outline(&out[entity_start..], rgb);
|
||||
} else {
|
||||
self.cull_entity_vertices(out, entity_start);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -288,8 +288,17 @@ fn mob_meta(
|
||||
defaults: &mut Vec<(u8, i32, Value)>,
|
||||
) {
|
||||
let kind = m.kind.id.as_str();
|
||||
if m.fire_ticks > 0 {
|
||||
meta.push((0, serializer::BYTE, Value::Byte(1)));
|
||||
// Entity.DATA_SHARED_FLAGS_ID: on fire 0x01, glowing 0x40
|
||||
// (LivingEntity.updateGlowingStatus).
|
||||
let shared = i8::from(m.fire_ticks > 0) | i8::from(m.is_currently_glowing()) << 6;
|
||||
if shared != 0 {
|
||||
meta.push((0, serializer::BYTE, Value::Byte(shared)));
|
||||
}
|
||||
// Entity.DATA_POSE: the pose the species keeps in its own fields (a
|
||||
// pose back at standing goes out by the entity's own default).
|
||||
let pose = horizon_client::mobs::sync::pose_id(m);
|
||||
if pose != 0 {
|
||||
meta.push((6, serializer::POSE, Value::Id(i32::from(pose))));
|
||||
}
|
||||
if let Some(name) = &m.custom_name {
|
||||
meta.push((
|
||||
@@ -400,6 +409,18 @@ fn mob_meta(
|
||||
}
|
||||
defaults.push((18, serializer::BYTE, Value::Byte(0)));
|
||||
}
|
||||
// Fox.DATA_TYPE_ID and DATA_FLAGS_ID (after AgeableMob's two; sleeping 32, ...).
|
||||
"minecraft:fox" => {
|
||||
if e.variant != 0 {
|
||||
meta.push((18, serializer::INT, Value::Int(i32::from(e.variant))));
|
||||
}
|
||||
defaults.push((18, serializer::INT, Value::Int(0)));
|
||||
let flags = (e.ints[0] & 0xff) as u8 as i8;
|
||||
if flags != 0 {
|
||||
meta.push((19, serializer::BYTE, Value::Byte(flags)));
|
||||
}
|
||||
defaults.push((19, serializer::BYTE, Value::Byte(0)));
|
||||
}
|
||||
"minecraft:cat" | "minecraft:wolf" => {
|
||||
// TamableAnimal.DATA_FLAGS_ID: sitting 1, tame 4.
|
||||
let flags = i8::from(e.sitting) | if e.tame { 4 } else { 0 };
|
||||
@@ -514,6 +535,29 @@ fn mob_meta(
|
||||
}
|
||||
defaults.push((17, serializer::BOOLEAN, Value::Bool(false)));
|
||||
}
|
||||
// Panda: UNHAPPY_COUNTER, SNEEZE_COUNTER and EAT_COUNTER (after
|
||||
// AgeableMob's two), MAIN_GENE_ID, HIDDEN_GENE_ID and DATA_ID_FLAGS;
|
||||
// the client animates the states from them.
|
||||
"minecraft:panda" => {
|
||||
let (flags, counters) = horizon_client::mobs::panda::get(m)
|
||||
.map_or((0, [0; 3]), |s| (s.flags, [s.unhappy, s.sneeze, s.eat]));
|
||||
for (index, value) in (18..).zip(counters) {
|
||||
if value != 0 {
|
||||
meta.push((index, serializer::INT, Value::Int(value)));
|
||||
}
|
||||
defaults.push((index, serializer::INT, Value::Int(0)));
|
||||
}
|
||||
for (index, gene) in [(21, e.variant), (22, e.variant2)] {
|
||||
if gene != 0 {
|
||||
meta.push((index, serializer::BYTE, Value::Byte(gene as i8)));
|
||||
}
|
||||
defaults.push((index, serializer::BYTE, Value::Byte(0)));
|
||||
}
|
||||
if flags != 0 {
|
||||
meta.push((23, serializer::BYTE, Value::Byte(flags as i8)));
|
||||
}
|
||||
defaults.push((23, serializer::BYTE, Value::Byte(0)));
|
||||
}
|
||||
// PolarBear.DATA_STANDING_ID (after AgeableMob's two): the bear
|
||||
// rears up; the client animates it.
|
||||
"minecraft:polar_bear" => {
|
||||
@@ -529,6 +573,121 @@ fn mob_meta(
|
||||
}
|
||||
defaults.push((17, serializer::BYTE, Value::Byte(0)));
|
||||
}
|
||||
// Warden.CLIENT_ANGER_LEVEL: the heartbeat's pace.
|
||||
"minecraft:warden" => {
|
||||
let anger = e.warden.as_ref().map_or(0, |w| w.client_anger);
|
||||
if anger != 0 {
|
||||
meta.push((16, serializer::INT, Value::Int(anger)));
|
||||
}
|
||||
defaults.push((16, serializer::INT, Value::Int(0)));
|
||||
}
|
||||
// Sniffer.DATA_STATE (after AgeableMob's two).
|
||||
"minecraft:sniffer" => {
|
||||
if e.variant != 0 {
|
||||
meta.push((
|
||||
18,
|
||||
serializer::SNIFFER_STATE,
|
||||
Value::Id(i32::from(e.variant)),
|
||||
));
|
||||
}
|
||||
defaults.push((18, serializer::SNIFFER_STATE, Value::Id(0)));
|
||||
}
|
||||
// Shulker.DATA_ATTACH_FACE_ID, DATA_PEEK_ID and DATA_COLOR_ID (16
|
||||
// for none).
|
||||
"minecraft:shulker" => {
|
||||
if e.variant3 != 0 {
|
||||
meta.push((16, serializer::DIRECTION, Value::Id(i32::from(e.variant3))));
|
||||
}
|
||||
defaults.push((16, serializer::DIRECTION, Value::Id(0)));
|
||||
if e.counter != 0 {
|
||||
meta.push((17, serializer::BYTE, Value::Byte(e.counter as i8)));
|
||||
}
|
||||
defaults.push((17, serializer::BYTE, Value::Byte(0)));
|
||||
if e.color != 16 {
|
||||
meta.push((18, serializer::BYTE, Value::Byte(e.color as i8)));
|
||||
}
|
||||
defaults.push((18, serializer::BYTE, Value::Byte(16)));
|
||||
}
|
||||
// Vex.DATA_FLAGS_ID: charging in bit 0 (the arms and the glowing
|
||||
// texture).
|
||||
"minecraft:vex" => {
|
||||
if e.flag {
|
||||
meta.push((16, serializer::BYTE, Value::Byte(1)));
|
||||
}
|
||||
defaults.push((16, serializer::BYTE, Value::Byte(0)));
|
||||
}
|
||||
// Creaking.CAN_MOVE, IS_ACTIVE and IS_TEARING_DOWN.
|
||||
"minecraft:creaking" => {
|
||||
if e.flag2 {
|
||||
meta.push((16, serializer::BOOLEAN, Value::Bool(false)));
|
||||
}
|
||||
defaults.push((16, serializer::BOOLEAN, Value::Bool(true)));
|
||||
if e.flag {
|
||||
meta.push((17, serializer::BOOLEAN, Value::Bool(true)));
|
||||
}
|
||||
defaults.push((17, serializer::BOOLEAN, Value::Bool(false)));
|
||||
if horizon_client::mobs::creaking::tearing_down(m) {
|
||||
meta.push((18, serializer::BOOLEAN, Value::Bool(true)));
|
||||
}
|
||||
defaults.push((18, serializer::BOOLEAN, Value::Bool(false)));
|
||||
}
|
||||
// Allay.DATA_DANCING and DATA_CAN_DUPLICATE (the held item is its
|
||||
// main hand equipment).
|
||||
"minecraft:allay" => {
|
||||
if e.flag {
|
||||
meta.push((16, serializer::BOOLEAN, Value::Bool(true)));
|
||||
}
|
||||
defaults.push((16, serializer::BOOLEAN, Value::Bool(false)));
|
||||
if e.egg_time > 0 {
|
||||
meta.push((17, serializer::BOOLEAN, Value::Bool(false)));
|
||||
}
|
||||
defaults.push((17, serializer::BOOLEAN, Value::Bool(true)));
|
||||
}
|
||||
// Axolotl.DATA_PLAYING_DEAD (after DATA_VARIANT).
|
||||
"minecraft:axolotl" => {
|
||||
if horizon_client::mobs::axolotl::is_playing_dead(m) {
|
||||
meta.push((19, serializer::BOOLEAN, Value::Bool(true)));
|
||||
}
|
||||
defaults.push((19, serializer::BOOLEAN, Value::Bool(false)));
|
||||
}
|
||||
// CopperGolem.COPPER_GOLEM_STATE (after DATA_WEATHER_STATE).
|
||||
"minecraft:copper_golem" => {
|
||||
if e.variant3 != 0 {
|
||||
meta.push((
|
||||
17,
|
||||
serializer::COPPER_GOLEM_STATE,
|
||||
Value::Id(i32::from(e.variant3)),
|
||||
));
|
||||
}
|
||||
defaults.push((17, serializer::COPPER_GOLEM_STATE, Value::Id(0)));
|
||||
}
|
||||
// Armadillo.ARMADILLO_STATE (after AgeableMob's two).
|
||||
"minecraft:armadillo" => {
|
||||
let state = horizon_client::mobs::armadillo::state(m);
|
||||
if state != 0 {
|
||||
meta.push((18, serializer::ARMADILLO_STATE, Value::Id(i32::from(state))));
|
||||
}
|
||||
defaults.push((18, serializer::ARMADILLO_STATE, Value::Id(0)));
|
||||
}
|
||||
// Frog.DATA_TONGUE_TARGET_ID (after the variant): the entity id plus
|
||||
// one, 0 for none.
|
||||
"minecraft:frog" => {
|
||||
if let Some(target) = e.frog.tongue_target {
|
||||
let id = i32::try_from(target).map_or(0, |t| t + 1);
|
||||
meta.push((19, serializer::OPTIONAL_UNSIGNED_INT, Value::Id(id)));
|
||||
}
|
||||
defaults.push((19, serializer::OPTIONAL_UNSIGNED_INT, Value::Id(0)));
|
||||
}
|
||||
// Goat.DATA_HAS_LEFT_HORN and DATA_HAS_RIGHT_HORN (after
|
||||
// DATA_IS_SCREAMING_GOAT): both true by default.
|
||||
"minecraft:goat" => {
|
||||
for (index, lost) in [(19, e.lost_horns[0]), (20, e.lost_horns[1])] {
|
||||
if lost {
|
||||
meta.push((index, serializer::BOOLEAN, Value::Bool(false)));
|
||||
}
|
||||
defaults.push((index, serializer::BOOLEAN, Value::Bool(true)));
|
||||
}
|
||||
}
|
||||
// Enderman.DATA_CARRY_STATE.
|
||||
"minecraft:enderman" => {
|
||||
if let Some(state) = e.carried {
|
||||
@@ -541,6 +700,38 @@ fn mob_meta(
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
// Camel.DASH and LAST_POSE_CHANGE_TICK (after AbstractHorse's flags).
|
||||
if horizon_client::mobs::camel::is_camel(m.species()) {
|
||||
if e.flag2 {
|
||||
meta.push((19, serializer::BOOLEAN, Value::Bool(true)));
|
||||
}
|
||||
defaults.push((19, serializer::BOOLEAN, Value::Bool(false)));
|
||||
let tick = horizon_client::mobs::camel::synced_pose_tick(m, game_time);
|
||||
if tick != 0 {
|
||||
meta.push((20, serializer::LONG, Value::Long(tick)));
|
||||
}
|
||||
defaults.push((20, serializer::LONG, Value::Long(0)));
|
||||
}
|
||||
}
|
||||
|
||||
/// What a client shakes a mob by: Entity.DATA_TICKS_FROZEN (7), whose full
|
||||
/// count is `isFullyFrozen`, and Strider.DATA_SUFFOCATING (19, after
|
||||
/// AgeableMob's two and DATA_BOOST_TIME).
|
||||
fn mob_freeze_meta(
|
||||
m: &Mob,
|
||||
meta: &mut Vec<(u8, i32, Value)>,
|
||||
defaults: &mut Vec<(u8, i32, Value)>,
|
||||
) {
|
||||
if m.ticks_frozen > 0 {
|
||||
meta.push((7, serializer::INT, Value::Int(m.ticks_frozen)));
|
||||
}
|
||||
defaults.push((7, serializer::INT, Value::Int(0)));
|
||||
if m.kind.id == "minecraft:strider" {
|
||||
if m.extra.flag {
|
||||
meta.push((19, serializer::BOOLEAN, Value::Bool(true)));
|
||||
}
|
||||
defaults.push((19, serializer::BOOLEAN, Value::Bool(false)));
|
||||
}
|
||||
}
|
||||
|
||||
/// The equipment a mob shows (ClientboundSetEquipmentPacket slots: main hand,
|
||||
@@ -551,13 +742,16 @@ fn mob_equipment(
|
||||
out: &mut Vec<(u8, Option<horizon_protocol::item::Stack>)>,
|
||||
) {
|
||||
let e = &m.extra;
|
||||
// A horse's armour, a llama's carpet, a harness: Horizon keeps the body
|
||||
// slot's item (6) in `head`.
|
||||
let body = horizon_client::mobs::drops::head_is_body(m.species());
|
||||
let slots = [
|
||||
(0u8, &e.main_hand),
|
||||
(1, &e.off_hand),
|
||||
(2, &e.feet),
|
||||
(3, &e.legs),
|
||||
(4, &e.chest),
|
||||
(5, &e.head),
|
||||
(if body { 6 } else { 5 }, &e.head),
|
||||
];
|
||||
for (slot, item) in slots {
|
||||
if let Some(item) = item {
|
||||
@@ -1067,6 +1261,7 @@ impl Game<'_> {
|
||||
&mut e.meta,
|
||||
&mut e.defaults,
|
||||
);
|
||||
mob_freeze_meta(m, &mut e.meta, &mut e.defaults);
|
||||
mob_equipment(m, hub, &mut e.equipment);
|
||||
self.mob_attributes(m, &mut e.attributes);
|
||||
e.leash = leash;
|
||||
@@ -1077,6 +1272,10 @@ impl Game<'_> {
|
||||
e.position = [d.position.x, d.position.y, d.position.z];
|
||||
e.on_ground = d.on_ground;
|
||||
e.velocity = [d.velocity.x, d.velocity.y, d.velocity.z];
|
||||
// Entity.DATA_SHARED_FLAGS_ID: glowing 0x40 (the Glowing tag).
|
||||
if self.entity_glowing(d.id) {
|
||||
e.meta.push((0, serializer::BYTE, Value::Byte(0x40)));
|
||||
}
|
||||
e.meta.push((8, serializer::ITEM_STACK, Value::Item(stack)));
|
||||
}
|
||||
for o in &self.orbs {
|
||||
@@ -1406,8 +1605,9 @@ impl Game<'_> {
|
||||
horizon_world::level::LevelEvent::LevelEvent { id, pos, data } => {
|
||||
// The acting guest does not predict the egg's teleport
|
||||
// particles, bone meal's growth, or anvil result sounds
|
||||
// (these levelEvent calls name no excluded player).
|
||||
let except = if matches!(*id, 2015 | 1505 | 1029 | 1030) {
|
||||
// (these levelEvent calls name no excluded player), nor
|
||||
// the digger's own crack and hit sound (2019, 2020).
|
||||
let except = if matches!(*id, 2015 | 1505 | 1029 | 1030 | 2019 | 2020) {
|
||||
None
|
||||
} else {
|
||||
self.lan_acting
|
||||
@@ -1630,6 +1830,7 @@ impl Game<'_> {
|
||||
.each_ref()
|
||||
.map(|s| s.as_ref().map(|s| s.item.clone()));
|
||||
m.extra.crouching = g.sneaking;
|
||||
m.extra.avatar_pose = g.pose();
|
||||
m.extra.sprinting = g.sprinting;
|
||||
// LivingEntity.DATA_EFFECT_PARTICLES: the swirls of its visible effects.
|
||||
let visible = || g.effects.list.iter().filter(|e| e.visible);
|
||||
@@ -1698,7 +1899,12 @@ impl Game<'_> {
|
||||
out,
|
||||
);
|
||||
self.held_item_geometry(m, camera, alpha, packed, true, out);
|
||||
if !m.glowing {
|
||||
// LivingEntity.isCurrentlyGlowing: the guest's Glowing effect.
|
||||
let guest = host.hub.guest(*conn);
|
||||
if m.glowing || guest.is_some_and(|g| g.effects.get("glowing").is_some()) {
|
||||
let team = guest.and_then(|g| self.score.board.players_team(&g.name));
|
||||
self.mark_outline(&out[entity_start..], super::glowing::team_color(team));
|
||||
} else {
|
||||
self.cull_entity_vertices(out, entity_start);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1200,6 +1200,7 @@ impl Game<'_> {
|
||||
}
|
||||
S::RenameItem(p) => self.lan_station_rename(host, conn, &p.name),
|
||||
S::SignUpdate(p) => self.lan_sign_update(host, conn, p),
|
||||
S::SetCommandBlock(p) => self.lan_set_command_block(host, conn, p),
|
||||
S::EditBook(p) => self.lan_edit_book(host, conn, p),
|
||||
S::ContainerSlotStateChanged(p) => {
|
||||
self.lan_crafter_slot_state(host, conn, p.container, p.slot, p.enabled);
|
||||
@@ -2188,7 +2189,7 @@ impl Game<'_> {
|
||||
}
|
||||
let in_wall = !g.sleeping && self.in_wall(g.position, g.eye_height());
|
||||
let at = g.position;
|
||||
let sneaking = g.sneaking;
|
||||
let (sneaking, sleeping) = (g.sneaking, g.sleeping);
|
||||
let mut soul = None;
|
||||
let events = self.as_guest(host, conn, |game| {
|
||||
let (rules, motion) = (game.world.health_rules, game.motion);
|
||||
@@ -2257,6 +2258,7 @@ impl Game<'_> {
|
||||
// Entity.applyEffectsFromBlocks along those moves: magma, cactus,
|
||||
// lit campfires and grown sweet berry bushes hurt (#1305).
|
||||
w.player.shift_key_down = sneaking;
|
||||
let _ = w.player.set_sleeping(sleeping);
|
||||
w.player.freeze_immune = freeze_immune;
|
||||
let _ = w.server_block_damage(start);
|
||||
let _ = w.update_soul_speed_block();
|
||||
@@ -2273,6 +2275,8 @@ impl Game<'_> {
|
||||
game.cauldron_extinguish(*pos);
|
||||
}
|
||||
}
|
||||
// Entity.baseTick: rain puts out a burning player.
|
||||
game.rain_extinguish_player();
|
||||
// WitherRoseBlock.entityInside along those moves.
|
||||
for event in &events {
|
||||
if matches!(event, MotionEvent::InsideWitherRose) {
|
||||
@@ -2333,6 +2337,15 @@ impl Game<'_> {
|
||||
speed,
|
||||
}) => self.landing_dust(Some(host), state, position, count, speed),
|
||||
MotionEvent::Fall(event) => self.landing_block_updates(&event),
|
||||
// LivingEntity.baseTick's entity event 67, to every client
|
||||
// around, the guest's own included; this window draws it too.
|
||||
MotionEvent::Drown => {
|
||||
if let Some(g) = host.hub.guest(conn) {
|
||||
host.hub
|
||||
.entity_event(g.entity_id, super::combat::DROWN_EVENT, at);
|
||||
}
|
||||
self.drown_particles(at, [0.0; 3]);
|
||||
}
|
||||
// HoneyBlock.maybeDoSlideEffects: the sound reaches the
|
||||
// others (Player.playSound excludes the player, whose client
|
||||
// plays it), the entity event 53 everyone around.
|
||||
@@ -2668,8 +2681,7 @@ impl Game<'_> {
|
||||
/// with Mending and gives the rest as points. Everyone near hears it.
|
||||
fn guest_orbs(&mut self, host: &mut LanHost, gi: usize) {
|
||||
let g = &host.hub.guests[gi];
|
||||
// Player's standing and crouching boxes: 1.8 and 1.5 high.
|
||||
let height = if g.sneaking { 1.5 } else { 1.8 };
|
||||
let height = f64::from(g.dimensions().height);
|
||||
let (conn, id, at) = (g.conn, g.entity_id, g.position);
|
||||
let unable = g.spectator() || g.dead || g.health <= 0.0;
|
||||
let play = host.play.entry(conn).or_default();
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
//! (hits, pushes, fire, its projectiles) runs with that guest swapped in.
|
||||
use super::{Game, lan::LanHost};
|
||||
use horizon_client::{
|
||||
mobs::{Mob, MobEvent, PlayerView, Who},
|
||||
mobs::{Mob, MobEvent, PlayerView, Species, Who},
|
||||
projectiles::{Hit, Projectile},
|
||||
};
|
||||
use horizon_core::geometry::{Aabb, Vec3};
|
||||
@@ -28,6 +28,22 @@ pub(super) fn player_for(
|
||||
) -> Option<u32> {
|
||||
let targeting = m.ai.target == Some(Who::Player)
|
||||
|| m.ai.last_hurt_by == Some(Who::Player) && m.age as i32 - m.ai.last_hurt_by_time <= 100;
|
||||
// A tame wolf or cat sees its owner (`TamableAnimal.getOwner`), while they
|
||||
// are in play, whoever is nearer. Not while it targets or was just hurt by
|
||||
// a player: then `Who::Player` is that attacker, whoever it is, and the
|
||||
// owner goals ignore it until the wolf lets go.
|
||||
if !targeting
|
||||
&& matches!(m.species(), Species::Wolf | Species::Cat)
|
||||
&& m.extra.tame
|
||||
&& let Some(owner) = m.extra.owner
|
||||
{
|
||||
if host.is_some_and(|v| v.alive && v.uuid == owner) {
|
||||
return None;
|
||||
}
|
||||
if let Some((conn, _)) = guests.iter().find(|(_, v)| v.alive && v.uuid == owner) {
|
||||
return Some(*conn);
|
||||
}
|
||||
}
|
||||
choose(m.position, targeting, host, guests, bound)
|
||||
}
|
||||
|
||||
@@ -70,6 +86,10 @@ pub(super) fn acts_on_player(e: &MobEvent) -> bool {
|
||||
target: Who::Player,
|
||||
..
|
||||
}
|
||||
| MobEvent::Knockback {
|
||||
target: Who::Player,
|
||||
..
|
||||
}
|
||||
| MobEvent::Ignite {
|
||||
target: Who::Player,
|
||||
..
|
||||
@@ -175,9 +195,9 @@ impl Game<'_> {
|
||||
.iter()
|
||||
.filter(|g| host.hub.here(g) && g.loaded && !g.dead && !g.spectator())
|
||||
.map(|g| {
|
||||
let [x, y, z] = g.position;
|
||||
let height = if g.sneaking { 1.5 } else { 1.8 };
|
||||
([x - 0.3, y, z - 0.3], [x + 0.3, y + height, z + 0.3])
|
||||
let b = g.bounds();
|
||||
let (lo, hi) = (b.min_position(), b.max_position());
|
||||
([lo.x, lo.y, lo.z], [hi.x, hi.y, hi.z])
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
@@ -319,7 +339,7 @@ impl Game<'_> {
|
||||
uuid: horizon_client::lan::playerdata::uuid_ints(g.uuid),
|
||||
position: g.position,
|
||||
eye: [x, y + g.eye_height(), z],
|
||||
height: if g.sneaking { 1.5 } else { 1.8 },
|
||||
height: g.dimensions().height,
|
||||
creative: g.creative(),
|
||||
spectator: g.spectator(),
|
||||
alive: !g.dead,
|
||||
@@ -336,14 +356,9 @@ impl Game<'_> {
|
||||
}
|
||||
}
|
||||
|
||||
/// A guest's bounding box (0.6 wide, 1.8 tall, 1.5 crouching).
|
||||
/// A guest's bounding box, by its pose (`Avatar.POSES`).
|
||||
pub(super) fn guest_bounds(g: &horizon_client::lan::guest::Guest) -> Aabb {
|
||||
let [x, y, z] = g.position;
|
||||
let height = if g.sneaking { 1.5 } else { 1.8 };
|
||||
Aabb::new(
|
||||
Vec3::new(x - 0.3, y, z - 0.3),
|
||||
Vec3::new(x + 0.3, y + height, z + 0.3),
|
||||
)
|
||||
g.bounds()
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
@@ -369,6 +384,8 @@ mod tests {
|
||||
last_hurt: None,
|
||||
velocity: Vec3::ZERO,
|
||||
yaw: 0.0,
|
||||
sprinting: false,
|
||||
riding: false,
|
||||
hero: false,
|
||||
effects: Vec::new(),
|
||||
uuid: [0; 4],
|
||||
|
||||
@@ -247,14 +247,10 @@ impl Game<'_> {
|
||||
return;
|
||||
};
|
||||
let (d, s) = (p.dist.map(f64::from), p.max_speed.map(f64::from));
|
||||
// ClientLevel.addParticle: the camera's 32 blocks unless the packet
|
||||
// (`force`) or the type overrides the limiter.
|
||||
// ClientLevel.addParticle: the camera's 32 blocks and the Particles
|
||||
// option, unless the packet or the type overrides the limiter.
|
||||
let add = |particles: &mut horizon_client::particles::ParticleEngine, at, v| {
|
||||
if p.override_limiter {
|
||||
particles.create(&options, at, v);
|
||||
} else {
|
||||
particles.add(&options, at, v);
|
||||
}
|
||||
particles.add_with(&options, at, v, p.override_limiter, p.always_show);
|
||||
};
|
||||
if p.count == 0 {
|
||||
add(
|
||||
|
||||
@@ -5,14 +5,10 @@
|
||||
//! forcing the game mode. Its difficulty lock asks first (PopupScreen), and
|
||||
//! Restrictions opens RestrictionsScreen.
|
||||
//! Parity reference: https://mcsrc.dev/2/26.3/net/minecraft/client/gui/screens/WorldOptionsScreen
|
||||
use super::{
|
||||
Game, Screen,
|
||||
game_rules_screen::{Exit, RulesScreen},
|
||||
integrated::ServerOptions,
|
||||
lan::LanSettings,
|
||||
};
|
||||
use super::{Game, Screen, integrated::ServerOptions, lan::LanSettings};
|
||||
use horizon_client::{
|
||||
font::Font,
|
||||
game_rules_screen::{Exit, RulesScreen},
|
||||
hud::{HudBatch, rgba},
|
||||
text::{Text, named_color},
|
||||
ui::{self, EditBox},
|
||||
|
||||
@@ -10,13 +10,12 @@ use horizon_world::level::LevelEvent;
|
||||
|
||||
impl Game<'_> {
|
||||
pub(super) fn dragon_egg_particles(&mut self, pos: [i32; 3], data: i32) {
|
||||
self.particles
|
||||
.block_teleportation(pos, data, [16, 8, 16], self.settings.particles);
|
||||
self.particles.block_teleportation(pos, data, [16, 8, 16]);
|
||||
}
|
||||
/// Event 2017: `TeleportRandomlyConsumeEffect`'s directional particles.
|
||||
pub(super) fn consume_teleport_particles(&mut self, pos: [i32; 3], data: i32) {
|
||||
self.particles
|
||||
.block_teleportation(pos, data, [127, 127, 127], self.settings.particles);
|
||||
.block_teleportation(pos, data, [127, 127, 127]);
|
||||
}
|
||||
/// `LevelEventHandler.levelEvent` 3002 to 3005, drawing from the client
|
||||
/// level's random.
|
||||
@@ -135,6 +134,30 @@ impl Game<'_> {
|
||||
// Each guest plays it from the global packet.
|
||||
self.audio.play_unshared(sound, at, volume, 1.0);
|
||||
}
|
||||
/// `ClientLevel.addBreakingBlockEffects`: the block's hit sound (only for
|
||||
/// `play_sound`) and one crumb on the face being mined; nothing for air.
|
||||
pub(super) fn breaking_block_effects(
|
||||
&mut self,
|
||||
pos: [i32; 3],
|
||||
face: [i32; 3],
|
||||
play_sound: bool,
|
||||
) {
|
||||
let state = self.world.block(pos[0], pos[1], pos[2]);
|
||||
if self.res.facts().is_air(state) {
|
||||
return;
|
||||
}
|
||||
if play_sound {
|
||||
let sounds = self.res.facts().get(state).sounds.clone();
|
||||
// Each guest plays it from the level event itself.
|
||||
self.audio.play_unshared(
|
||||
&sounds.hit,
|
||||
Some(pos.map(|v| f64::from(v) + 0.5)),
|
||||
(sounds.volume + 1.0) / 8.0,
|
||||
sounds.pitch * 0.5,
|
||||
);
|
||||
}
|
||||
self.crack_particle(pos, face, state);
|
||||
}
|
||||
/// `LevelEventHandler.potionSplashParticles` (2002 with `Effect`, 2007
|
||||
/// with `InstantEffect`): eight splash potion crumbs, and a hundred
|
||||
/// spell particles in the potion's colour `data`, flung out in a disc.
|
||||
|
||||
@@ -324,6 +324,13 @@ const DECORATION_TYPES: [(&str, &str); 24] = [
|
||||
("mineshaft", "map/decorations/mineshaft"),
|
||||
("ocean_ruin_warm", "map/decorations/warm_ocean_ruins"),
|
||||
];
|
||||
/// `MapDecorationType.showOnItemFrame` is false for the player arrows and the two markers.
|
||||
fn shown_on_frame(sprite: &str) -> bool {
|
||||
!matches!(
|
||||
decoration_type(sprite),
|
||||
Some("player" | "red_marker" | "blue_marker" | "player_off_map" | "player_off_limits")
|
||||
)
|
||||
}
|
||||
/// The sprite of a decoration type that a `minecraft:map_decorations`
|
||||
/// entry or a server's packet names.
|
||||
fn decoration_sprite(id: &str) -> Option<&'static str> {
|
||||
@@ -1114,9 +1121,12 @@ impl Game<'_> {
|
||||
}
|
||||
/// `MapRenderer.render`: the map texture and its decorations under `m`
|
||||
/// (map pixels 0..128, y down), drawn at depth offsets toward the viewer.
|
||||
/// `show_only_frame` (a map in an item frame) leaves out the decorations that
|
||||
/// do not show on frames.
|
||||
pub(super) fn map_vertices(
|
||||
&mut self,
|
||||
id: i32,
|
||||
show_only_frame: bool,
|
||||
m: glam::Mat4,
|
||||
light: u32,
|
||||
out: &mut Vec<Vertex>,
|
||||
@@ -1158,14 +1168,18 @@ impl Game<'_> {
|
||||
.get(&id)
|
||||
.map(|m| m.decorations.clone())
|
||||
.unwrap_or_default();
|
||||
for (i, d) in decorations.iter().enumerate() {
|
||||
let mut count = 0;
|
||||
for d in &decorations {
|
||||
if show_only_frame && !shown_on_frame(d.sprite) {
|
||||
continue;
|
||||
}
|
||||
let Some(s) = self.gui.atlas.sprites.get(d.sprite).copied() else {
|
||||
continue;
|
||||
};
|
||||
let dm = glam::Mat4::from_translation(glam::Vec3::new(
|
||||
f32::from(d.x) / 2.0 + 64.0,
|
||||
f32::from(d.y) / 2.0 + 64.0,
|
||||
-0.02 - i as f32 * 0.001,
|
||||
-0.02 - count as f32 * 0.001,
|
||||
)) * glam::Mat4::from_rotation_z(
|
||||
(f32::from(d.rot) * 360.0 / 16.0).to_radians(),
|
||||
) * glam::Mat4::from_scale(glam::Vec3::splat(4.0))
|
||||
@@ -1187,6 +1201,7 @@ impl Game<'_> {
|
||||
[p(-1.0, 1.0), p(1.0, 1.0), p(1.0, -1.0), p(-1.0, -1.0)],
|
||||
[[s.u0, s.v0], [s.u1, s.v0], [s.u1, s.v1], [s.u0, s.v1]],
|
||||
);
|
||||
count += 1;
|
||||
}
|
||||
}
|
||||
/// `renderTwoHandedMap`: map `id` held in both hands, in view space (the
|
||||
@@ -1255,7 +1270,7 @@ impl Game<'_> {
|
||||
}
|
||||
}
|
||||
if has_data {
|
||||
self.map_vertices(id, m, light, out);
|
||||
self.map_vertices(id, false, m, light, out);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -713,12 +713,7 @@ impl Game<'_> {
|
||||
host.hub.here(g) && g.loaded && !g.dead && !g.spectator() && g.vehicle.is_none()
|
||||
}) {
|
||||
let [x, y, z] = g.position;
|
||||
let height = if g.sneaking { 1.5 } else { 1.8 };
|
||||
let bb = Aabb::new(
|
||||
Vec3::new(x - 0.3, y, z - 0.3),
|
||||
Vec3::new(x + 0.3, y + height, z + 0.3),
|
||||
);
|
||||
pushers.push((Vec3::new(x, y, z), bb));
|
||||
pushers.push((Vec3::new(x, y, z), g.bounds()));
|
||||
}
|
||||
}
|
||||
for (feet, bb) in pushers {
|
||||
|
||||
@@ -384,13 +384,15 @@ impl Game<'_> {
|
||||
owner.use_item_remaining_ticks = 72_000 - mob.extra.using_item;
|
||||
owner.crossbow_charge_duration = 25;
|
||||
}
|
||||
let context = if left {
|
||||
horizon_assets::item_model::DisplayContext::ThirdPersonLeftHand
|
||||
} else {
|
||||
horizon_assets::item_model::DisplayContext::ThirdPersonRightHand
|
||||
use horizon_assets::item_model::DisplayContext as Context;
|
||||
// PandaHoldsItemLayer shows the item as it lies on the ground.
|
||||
let (context, ordinal) = match (mob.species(), left) {
|
||||
(Species::Panda, _) => (Context::Ground, 7),
|
||||
(_, true) => (Context::ThirdPersonLeftHand, 1),
|
||||
(_, false) => (Context::ThirdPersonRightHand, 2),
|
||||
};
|
||||
// `updateForLiving`: seeded by the entity id plus the context ordinal.
|
||||
let seed = mob.id as i32 + if left { 1 } else { 2 };
|
||||
let seed = mob.id as i32 + ordinal;
|
||||
let state = self.resolve_item(&stack, context, Some(&owner), seed);
|
||||
if lit {
|
||||
self.submit_item_world(&state, t, light, out);
|
||||
|
||||
@@ -338,7 +338,9 @@ impl Game<'_> {
|
||||
let (r, u) = (right.as_vec3() * 0.025, up.as_vec3() * 0.025);
|
||||
// Text space: x right, y down (scale 0.025, -0.025).
|
||||
let at = |x: f32, y: f32| o + r * x - u * y;
|
||||
let chars: Vec<(char, u32)> = text
|
||||
// Component.getVisualOrderText.
|
||||
let shown = horizon_client::bidi::visual(text);
|
||||
let chars: Vec<(char, u32)> = shown
|
||||
.spans
|
||||
.iter()
|
||||
.flat_map(|s| {
|
||||
|
||||
@@ -0,0 +1,165 @@
|
||||
//! Pandas on the game side: the food they find on the ground and pick up
|
||||
//! (`Mob.aiStep`, `Panda.pickUpItem`), the bamboo a player gives them
|
||||
//! (`Panda.mobInteract`), the start a sneeze gives the grown pandas about,
|
||||
//! and the crumbs and the sneeze a window draws.
|
||||
use super::{
|
||||
Game,
|
||||
pickups::{Collector, Taken},
|
||||
};
|
||||
use horizon_client::{
|
||||
items::ItemStack,
|
||||
mobs::{Species, panda},
|
||||
particles::{ParticleOptions, ParticleType},
|
||||
};
|
||||
use horizon_core::geometry::Vec3;
|
||||
|
||||
impl Game<'_> {
|
||||
/// `Panda.canPickUpAndEat`'s tag.
|
||||
fn panda_eats_from_ground(&self, item: &str) -> bool {
|
||||
self.res
|
||||
.items()
|
||||
.item_tags
|
||||
.get("minecraft:panda_eats_from_ground")
|
||||
.is_some_and(|tag| tag.contains(item))
|
||||
}
|
||||
/// A panda takes what lies at its feet when its mouth is free, and
|
||||
/// looks for food within 6 and 8 blocks for `PandaSitGoal`.
|
||||
pub(super) fn tick_pandas(&mut self) {
|
||||
let griefing = self.ui.rules.bool("mob_griefing");
|
||||
for i in 0..self.mobs.len() {
|
||||
let m = &self.mobs[i];
|
||||
if m.species() != Species::Panda || !m.alive() {
|
||||
continue;
|
||||
}
|
||||
let id = m.id;
|
||||
let wants = |g: &Self, e: &horizon_client::entities::ItemEntity| {
|
||||
e.pickup_delay == 0 && g.panda_eats_from_ground(&e.stack.item)
|
||||
};
|
||||
// Mob.aiStep: the items within the box grown by (1, 0, 1).
|
||||
if griefing && m.can_pick_up_loot() && m.extra.main_hand.is_none() {
|
||||
let reach = m.bounds().inflate(Vec3::new(1.0, 0.0, 1.0));
|
||||
let taken = self
|
||||
.dropped
|
||||
.iter()
|
||||
.position(|e| wants(self, e) && reach.intersects(e.bounds()));
|
||||
if let Some(j) = taken {
|
||||
let e = self.dropped.remove(j);
|
||||
self.add_pickup(Taken::Item(Box::new(e.clone())), Collector::Mob(id));
|
||||
panda::hold(&mut self.mobs[i], &e.stack.item, e.stack.count, true);
|
||||
}
|
||||
}
|
||||
let m = &self.mobs[i];
|
||||
let area = |r: f64| m.bounds().inflate(Vec3::new(r, r, r));
|
||||
let (near, wide) = (area(6.0), area(8.0));
|
||||
let food_near = self
|
||||
.dropped
|
||||
.iter()
|
||||
.any(|e| wants(self, e) && near.intersects(e.bounds()));
|
||||
let food_at = self
|
||||
.dropped
|
||||
.iter()
|
||||
.find(|e| wants(self, e) && wide.intersects(e.bounds()))
|
||||
.map(|e| [e.position.x, e.position.y, e.position.z]);
|
||||
panda::set_food(&mut self.mobs[i], food_near, food_at);
|
||||
}
|
||||
}
|
||||
/// `Panda.mobInteract` for a click with `item`; whether the click is taken.
|
||||
pub(super) fn interact_panda(&mut self, index: usize, item: &str) -> bool {
|
||||
let creative = self.creative;
|
||||
let m = &mut self.mobs[index];
|
||||
if panda::scared(m) {
|
||||
return false;
|
||||
}
|
||||
if panda::get_up(m) {
|
||||
return true;
|
||||
}
|
||||
if !m.kind.food.iter().any(|f| f == item) {
|
||||
return false;
|
||||
}
|
||||
if m.ai.target.is_some() {
|
||||
panda::got_bamboo(m);
|
||||
}
|
||||
let at = [m.position.x, m.position.y, m.position.z];
|
||||
let mut dropped = None;
|
||||
if m.is_baby() {
|
||||
// ageUp((int) (-getAge() / 20 * 0.1F), true).
|
||||
let seconds = ((-m.extra.age / 20) as f32 * 0.1) as i32;
|
||||
horizon_client::mobs::ai::age_up(m, seconds, true);
|
||||
} else if m.extra.age == 0 && m.extra.in_love <= 0 {
|
||||
m.extra.in_love = 600;
|
||||
m.extra.love_player = true;
|
||||
} else {
|
||||
if panda::is_sitting(m) || m.fluids.in_water {
|
||||
return false;
|
||||
}
|
||||
panda::try_to_sit(m);
|
||||
panda::start_eating(m);
|
||||
// What it held falls, unless the player has infinite materials.
|
||||
if let Some(held) = m.extra.main_hand.clone()
|
||||
&& !creative
|
||||
{
|
||||
dropped = Some(ItemStack::new(&held, panda::held_count(m)));
|
||||
}
|
||||
panda::hold(m, item, 1, false);
|
||||
}
|
||||
self.consume_held();
|
||||
if let Some(stack) = dropped {
|
||||
self.spawn_item(stack, at, [0.0, 0.2, 0.0], 10);
|
||||
}
|
||||
true
|
||||
}
|
||||
/// `Panda.afterSneeze`: the grown pandas within 10 blocks of a cub jump.
|
||||
pub(super) fn panda_sneezed(&mut self, id: u32) {
|
||||
let Some(area) = self
|
||||
.mobs
|
||||
.iter()
|
||||
.find(|m| m.id == id)
|
||||
.map(|m| m.bounds().inflate(Vec3::new(10.0, 10.0, 10.0)))
|
||||
else {
|
||||
return;
|
||||
};
|
||||
for m in &mut self.mobs {
|
||||
if m.species() == Species::Panda
|
||||
&& !m.is_baby()
|
||||
&& m.on_ground
|
||||
&& !m.fluids.in_water
|
||||
&& panda::can_perform_action(m, panda::scared(m))
|
||||
&& area.intersects(m.bounds())
|
||||
{
|
||||
// LivingEntity.jumpFromGround.
|
||||
m.needs_sync = true;
|
||||
let power = f64::from(m.jump_power());
|
||||
if power > 1.0e-5 {
|
||||
m.velocity.y = power.max(m.velocity.y);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
/// What a window draws of its pandas: the crumbs of a bite
|
||||
/// (`addEatingParticles`) and the sneeze's particle.
|
||||
pub(super) fn panda_effects(&mut self) {
|
||||
let mut bites = Vec::new();
|
||||
let mut sneezes = Vec::new();
|
||||
for m in self
|
||||
.mobs
|
||||
.iter_mut()
|
||||
.filter(|m| m.species() == Species::Panda)
|
||||
{
|
||||
let (crumbs, sneeze) = panda::effects(m);
|
||||
if let (Some(crumbs), Some(item)) = (crumbs, m.extra.main_hand.clone()) {
|
||||
bites.push((item, crumbs));
|
||||
}
|
||||
sneezes.extend(sneeze);
|
||||
}
|
||||
for (item, crumbs) in bites {
|
||||
let options = self.item_particle(&ItemStack::new(&item, 1));
|
||||
for (at, velocity) in crumbs {
|
||||
self.particles.add(&options, at, velocity);
|
||||
}
|
||||
}
|
||||
let options = ParticleOptions::simple(ParticleType::Sneeze);
|
||||
for (at, velocity) in sneezes {
|
||||
self.particles.add(&options, at, velocity);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -443,6 +443,7 @@ impl Game<'_> {
|
||||
let packed = (u32::from(light >> 4) * 16) | ((u32::from(light & 15) * 16) << 8);
|
||||
let atlas = &self.gui.atlas;
|
||||
let lookup = |name: &str| atlas.sprites.get(name).map(|r| [r.u0, r.v0, r.u1, r.v1]);
|
||||
let start = out.len();
|
||||
horizon_client::mobs::render::render_with(
|
||||
m,
|
||||
alpha as f32,
|
||||
@@ -456,6 +457,15 @@ impl Game<'_> {
|
||||
self.player_elytra_geometry(m, camera, alpha as f32, packed, out);
|
||||
self.spin_attack_geometry(m, camera, alpha as f32, packed, out);
|
||||
self.held_item_geometry(m, camera, alpha, packed, true, out);
|
||||
// LivingEntity.isCurrentlyGlowing: the Glowing effect. A server's client is
|
||||
// in its team by its profile name.
|
||||
if self.potions.effects.get("glowing").is_some() {
|
||||
let team = match &self.remote {
|
||||
Some(r) => self.score.board.players_team(&r.profile_name),
|
||||
None => self.player_team(),
|
||||
};
|
||||
self.mark_outline(&out[start..], super::glowing::team_color(team));
|
||||
}
|
||||
}
|
||||
/// `extractEntityInInventoryFollowsMouse`: the player in the box
|
||||
/// `[x0, x1) x [y0, y1)`, `size` pixels per block, turned toward the
|
||||
|
||||
@@ -53,7 +53,11 @@ impl Game<'_> {
|
||||
if let Some(player) = self.player.as_mut() {
|
||||
player.fire_ticks = 0;
|
||||
}
|
||||
// Entity.playEntityOnFireExtinguishedSound
|
||||
self.extinguish_sound(at);
|
||||
Ok(())
|
||||
}
|
||||
/// Entity.playEntityOnFireExtinguishedSound
|
||||
fn extinguish_sound(&mut self, at: [f64; 3]) {
|
||||
let r = self.particles.random();
|
||||
let pitch = 1.6 + (r.next_float() - r.next_float()) * 0.4;
|
||||
self.audio.play(
|
||||
@@ -62,7 +66,29 @@ impl Game<'_> {
|
||||
0.7,
|
||||
pitch,
|
||||
);
|
||||
Ok(())
|
||||
}
|
||||
/// `Entity.applyEffectsFromBlocks`: a burning player in the rain
|
||||
/// (`isInRain`) has the fire put out (`clearFire`), with the extinguish
|
||||
/// sound that everyone near hears.
|
||||
pub(super) fn rain_extinguish_player(&mut self) {
|
||||
let Some(player) = self.player.as_ref() else {
|
||||
return;
|
||||
};
|
||||
let (at, top) = (player.position, player.bounds().max_position().y);
|
||||
if player.fire_ticks <= 0 || !self.in_rain(at, top) {
|
||||
return;
|
||||
}
|
||||
if let Some(player) = self.player.as_mut() {
|
||||
player.fire_ticks = 0;
|
||||
}
|
||||
self.extinguish_sound([at.x, at.y, at.z]);
|
||||
}
|
||||
/// The in-window player's rain extinguish (a server's player has its
|
||||
/// fire put out by the server).
|
||||
pub(super) fn rain_extinguish_in_frame(&mut self) {
|
||||
if self.remote.is_none() {
|
||||
self.rain_extinguish_player();
|
||||
}
|
||||
}
|
||||
/// Whether the block at `pos` is powder snow.
|
||||
pub(super) fn is_powder_snow(&self, pos: [i32; 3]) -> bool {
|
||||
|
||||
@@ -1036,6 +1036,7 @@ impl Game<'_> {
|
||||
/// read it).
|
||||
fn sync_villager_raids(&mut self, infos: &HashMap<i32, Vec<RaiderInfo>>) {
|
||||
let raids = &self.raiding.raids;
|
||||
let mut ongoing = Vec::new();
|
||||
for m in &mut self.mobs {
|
||||
if m.species() != Species::Villager {
|
||||
continue;
|
||||
@@ -1061,7 +1062,27 @@ impl Game<'_> {
|
||||
let ext = mobs_villager::ext_mut(m);
|
||||
ext.raid = status;
|
||||
ext.raid_wave = wave;
|
||||
if !m.no_ai && matches!(status, mobs_villager::PRE_RAID | mobs_villager::RAID) {
|
||||
ongoing.push(m.id);
|
||||
}
|
||||
}
|
||||
// Villager.customServerAiStep: a 1 in 100 chance each tick, in an
|
||||
// ongoing raid, of entity event 42 (`handleEntityEvent` splashes).
|
||||
for id in ongoing {
|
||||
if self.particles.random().next_int_bound(100) == Ok(0) {
|
||||
self.mob_raid_splash(id);
|
||||
}
|
||||
}
|
||||
}
|
||||
/// Entity event 42 for villager `id`: the guests get the byte, and this
|
||||
/// game draws `AbstractVillager.addParticlesAroundSelf(SPLASH)` itself.
|
||||
fn mob_raid_splash(&mut self, id: u32) {
|
||||
let Some(m) = self.mobs.iter().find(|m| m.id == id) else {
|
||||
return;
|
||||
};
|
||||
let at = [m.position.x, m.position.y, m.position.z];
|
||||
self.lan_entity_event(id as i32, 42, at);
|
||||
self.particles_around_mob(id, "splash", horizon_client::particles::Burst::VILLAGER);
|
||||
}
|
||||
|
||||
/// The raid bar a living player at `at` is in (`Raid.getPlayers`): red,
|
||||
|
||||
@@ -182,6 +182,9 @@ pub(super) struct Remote {
|
||||
online_mode: bool,
|
||||
/// Player entities in the level: entity id to profile UUID.
|
||||
level_players: HashMap<i32, u128>,
|
||||
/// Entities with shared flag 6 (`Entity.isCurrentlyGlowing` on a client); mobs keep
|
||||
/// theirs in `Mob::glowing`.
|
||||
glowing: std::collections::HashSet<i32>,
|
||||
/// The local player's profile UUID (always in the level).
|
||||
profile: u128,
|
||||
/// The local player's profile name: its score holder and team member.
|
||||
@@ -468,6 +471,7 @@ impl Game<'_> {
|
||||
listed: Default::default(),
|
||||
online_mode: false,
|
||||
level_players: HashMap::new(),
|
||||
glowing: Default::default(),
|
||||
profile: 0,
|
||||
profile_name: String::new(),
|
||||
destroying: None,
|
||||
@@ -1466,7 +1470,9 @@ impl Game<'_> {
|
||||
self.remove_remote_dragon(id);
|
||||
self.remove_remote_object(id as u32);
|
||||
self.remove_remote_projectile(id as u32);
|
||||
self.persist.uuids.remove(&(id as u32));
|
||||
if let Some(r) = self.remote.as_mut() {
|
||||
r.glowing.remove(&id);
|
||||
r.entities.remove(&id);
|
||||
r.lerps.remove(&id);
|
||||
r.stepped.remove(&id);
|
||||
@@ -1777,6 +1783,11 @@ impl Game<'_> {
|
||||
r.level_players.remove(&p.id);
|
||||
}
|
||||
}
|
||||
// Entity.getTeam: a mob is on a team by its UUID.
|
||||
self.persist.uuids.insert(
|
||||
p.id as u32,
|
||||
horizon_client::lan::playerdata::uuid_ints(p.uuid),
|
||||
);
|
||||
self.spawn_entity(p.id, &kind, p.position, rotation);
|
||||
// Entity.recreateFromPacket: setDeltaMovement(packet.getMovement()).
|
||||
self.remote_motion(p.id, p.velocity);
|
||||
@@ -1895,7 +1906,10 @@ impl Game<'_> {
|
||||
if let Some(m) = self.mobs.iter_mut().find(|m| m.id == p.id as u32) {
|
||||
for (index, _, value) in &p.values {
|
||||
match (index, value) {
|
||||
(0, Value::Byte(flags)) => m.invisible = flags & 0x20 != 0,
|
||||
(0, Value::Byte(flags)) => {
|
||||
m.invisible = flags & 0x20 != 0;
|
||||
m.glowing = flags & 0x40 != 0;
|
||||
}
|
||||
(2, Value::OptText(name)) => {
|
||||
let text = name.as_ref().map(horizon_client::entity_nbt::name_text);
|
||||
m.custom_name =
|
||||
@@ -1912,6 +1926,15 @@ impl Game<'_> {
|
||||
}
|
||||
}
|
||||
}
|
||||
if let Some((_, _, Value::Byte(flags))) = p.values.iter().find(|v| v.0 == 0)
|
||||
&& let Some(r) = self.remote.as_mut()
|
||||
{
|
||||
if flags & 0x40 != 0 {
|
||||
r.glowing.insert(p.id);
|
||||
} else {
|
||||
r.glowing.remove(&p.id);
|
||||
}
|
||||
}
|
||||
self.remote_mob_data(p.id, &p.values);
|
||||
if let Some(m) = self.mobs.iter_mut().find(|m| m.id == p.id as u32)
|
||||
&& m.species() == horizon_client::mobs::Species::Creeper
|
||||
@@ -1926,6 +1949,22 @@ impl Game<'_> {
|
||||
}
|
||||
}
|
||||
}
|
||||
if let Some(m) = self.mobs.iter_mut().find(|m| m.id == p.id as u32) {
|
||||
for (index, _, value) in &p.values {
|
||||
match (index, value) {
|
||||
// Entity.DATA_TICKS_FROZEN: isFullyFrozen shakes it.
|
||||
(7, Value::Int(ticks)) => m.ticks_frozen = *ticks,
|
||||
// Strider.DATA_SUFFOCATING (after AgeableMob's two
|
||||
// and DATA_BOOST_TIME): a cold strider shakes.
|
||||
(19, Value::Bool(cold))
|
||||
if m.species() == horizon_client::mobs::Species::Strider =>
|
||||
{
|
||||
m.extra.flag = *cold;
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
}
|
||||
// LivingEntity.DATA_LIVING_ENTITY_FLAGS: for another player
|
||||
// too, remote_mob_data's apply reads index 8 above.
|
||||
let mannequin = self
|
||||
@@ -2024,13 +2063,22 @@ impl Game<'_> {
|
||||
// player holds and wears (EquipmentSlot ordinals).
|
||||
C::SetEquipment(p) => {
|
||||
self.remote_object_equipment(p.id, &p.slots);
|
||||
let items: Vec<(u8, Option<Arc<str>>)> = p
|
||||
let items: Vec<(u8, Option<Arc<str>>, Option<ItemStack>)> = p
|
||||
.slots
|
||||
.iter()
|
||||
.map(|(slot, stack)| (*slot, stack.as_ref().map(|s| self.item_name(s.item))))
|
||||
.map(|(slot, stack)| {
|
||||
(
|
||||
*slot,
|
||||
stack.as_ref().map(|s| self.item_name(s.item)),
|
||||
self.to_stack(stack),
|
||||
)
|
||||
})
|
||||
.collect();
|
||||
if let Some(m) = self.mobs.iter_mut().find(|m| m.id == p.id as u32) {
|
||||
for (slot, item) in items {
|
||||
// The body slot (6: horse armour, llama carpet, harness)
|
||||
// is kept in `head`, as Horizon's own mobs do.
|
||||
let body = horizon_client::mobs::drops::head_is_body(m.species());
|
||||
for (slot, item, stack) in items {
|
||||
let e = &mut m.extra;
|
||||
match slot {
|
||||
0 => e.main_hand = item,
|
||||
@@ -2038,7 +2086,9 @@ impl Game<'_> {
|
||||
2 => e.feet = item,
|
||||
3 => e.legs = item,
|
||||
4 => e.chest = item,
|
||||
5 => e.head = item,
|
||||
5 if !body => e.head = item,
|
||||
6 if body => e.head = item,
|
||||
7 => m.set_saddle(stack),
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
@@ -2147,6 +2197,10 @@ impl Game<'_> {
|
||||
{
|
||||
m.health = 0.0;
|
||||
}
|
||||
// The events that start a mob's animation (the warden's).
|
||||
if let Some(m) = self.mobs.iter_mut().find(|m| m.id == p.id as u32) {
|
||||
horizon_client::mobs::sync::entity_event(m, p.event as u8);
|
||||
}
|
||||
// LivingEntity.handleEntityEvent 60: the poof as the dead body goes.
|
||||
if p.event == 60
|
||||
&& let Some(m) = self.mobs.iter().find(|m| m.id == p.id as u32)
|
||||
@@ -2157,6 +2211,9 @@ impl Game<'_> {
|
||||
if p.event == super::totem::TOTEM_EVENT {
|
||||
self.remote_totem(p.id);
|
||||
}
|
||||
if p.event == super::combat::DROWN_EVENT {
|
||||
self.remote_drown(p.id);
|
||||
}
|
||||
// Entity event 53: the honey slide particles of a player
|
||||
// sliding down a honey block (HoneyBlock.showSlideParticles).
|
||||
if p.event == 53 {
|
||||
@@ -3216,6 +3273,7 @@ impl Game<'_> {
|
||||
self.border.tick();
|
||||
self.player_border();
|
||||
self.tick_remote_entities(running);
|
||||
self.tick_remote_guardian_eyes(running);
|
||||
// ClientLevel.tickBlockEntities: chest lids, shulker boxes, bells,
|
||||
// and the moving pistons a block event's client-side moveBlocks made
|
||||
// (their progress advances between the server's block updates).
|
||||
@@ -3348,13 +3406,12 @@ impl Game<'_> {
|
||||
let events = std::mem::take(&mut player.events);
|
||||
self.previous_eye = self.eye;
|
||||
self.eye += (player.dimensions.eye_height - self.eye) * 0.5;
|
||||
let crouching = player.pose == horizon_client::pose::Pose::Crouching;
|
||||
let feet = [player.position.x, player.position.y, player.position.z];
|
||||
for event in events {
|
||||
match event {
|
||||
horizon_client::motion::MotionEvent::Emission {
|
||||
state, in_water, ..
|
||||
} if !crouching => {
|
||||
} => {
|
||||
let sounds = self.res.facts().get(state).sounds.clone();
|
||||
// Player.playStepSound: muffled under the swim sound in water.
|
||||
let (volume, pitch) = if in_water {
|
||||
@@ -3368,6 +3425,14 @@ impl Game<'_> {
|
||||
self.audio
|
||||
.play("minecraft:entity.player.swim", Some(feet), volume, pitch);
|
||||
}
|
||||
horizon_client::motion::MotionEvent::Chime { volume, pitch } => {
|
||||
self.audio.play(
|
||||
"minecraft:block.amethyst_block.chime",
|
||||
Some(feet),
|
||||
volume,
|
||||
pitch,
|
||||
);
|
||||
}
|
||||
// NetherPortalBlock.entityInside on the client's own level:
|
||||
// the portal overlay and its trigger sound (the server moves
|
||||
// the player).
|
||||
@@ -3510,6 +3575,43 @@ impl Game<'_> {
|
||||
}
|
||||
/// ClientLevel.tickEntities; a frozen level only ticks the players
|
||||
/// (`TickRateManager.isEntityFrozen`).
|
||||
/// `GuardianRenderer.getEntityToLookAt`: a guardian's eye follows its
|
||||
/// synched attack target, else the camera.
|
||||
fn tick_remote_guardian_eyes(&mut self, running: bool) {
|
||||
use horizon_client::mobs::{Species, Who, guardian};
|
||||
if !running {
|
||||
return;
|
||||
}
|
||||
let camera = self.player.as_ref().map(|p| {
|
||||
[
|
||||
p.position.x,
|
||||
p.position.y + f64::from(p.dimensions.eye_height),
|
||||
p.position.z,
|
||||
]
|
||||
});
|
||||
let looks: Vec<_> = self
|
||||
.mobs
|
||||
.iter()
|
||||
.enumerate()
|
||||
.filter(|(_, m)| {
|
||||
matches!(m.species(), Species::Guardian | Species::ElderGuardian) && m.alive()
|
||||
})
|
||||
.filter_map(|(i, m)| {
|
||||
let look = match m.ai.target.filter(|_| m.extra.flag) {
|
||||
Some(Who::Mob(id)) => self
|
||||
.mobs
|
||||
.iter()
|
||||
.find(|t| t.id == id)
|
||||
.map(|t| [t.position.x, t.eye_y(), t.position.z]),
|
||||
_ => camera,
|
||||
};
|
||||
look.map(|look| (i, look))
|
||||
})
|
||||
.collect();
|
||||
for (i, look) in looks {
|
||||
guardian::look_at(&mut self.mobs[i], look);
|
||||
}
|
||||
}
|
||||
fn tick_remote_entities(&mut self, running: bool) {
|
||||
if running {
|
||||
self.tick_text_displays();
|
||||
@@ -3518,22 +3620,25 @@ impl Game<'_> {
|
||||
// server never sends the fluid contact, and the renderers read it (cod, salmon and
|
||||
// tropical fish lie on their side without `in_water`, #1591).
|
||||
// A frozen level ticks only the players, as below.
|
||||
let fluids: Vec<_> = {
|
||||
let (fluids, thundering): (Vec<_>, bool) = {
|
||||
let w = self.mob_world_base(&[]);
|
||||
let players = self.remote.as_ref().map(|r| &r.level_players);
|
||||
self.mobs
|
||||
let fluids = self
|
||||
.mobs
|
||||
.iter()
|
||||
.map(|m| {
|
||||
(running || players.is_some_and(|p| p.contains_key(&(m.id as i32))))
|
||||
.then(|| w.fluids(m.bounds(), m.eye_y()))
|
||||
})
|
||||
.collect()
|
||||
.collect();
|
||||
(fluids, w.thundering)
|
||||
};
|
||||
for (m, fluids) in self.mobs.iter_mut().zip(fluids) {
|
||||
if let Some(fluids) = fluids {
|
||||
m.fluids = fluids;
|
||||
}
|
||||
}
|
||||
let game_time = self.world.game_time;
|
||||
let Some(r) = self.remote.as_mut() else {
|
||||
return;
|
||||
};
|
||||
@@ -3542,11 +3647,15 @@ impl Game<'_> {
|
||||
let head_lerps = &mut r.head_lerps;
|
||||
let players = &r.level_players;
|
||||
let mut using = Vec::new();
|
||||
let mut wither_heads = Vec::new();
|
||||
for m in &mut self.mobs {
|
||||
let player = players.contains_key(&(m.id as i32));
|
||||
if !running && !player {
|
||||
continue;
|
||||
}
|
||||
if m.species() == horizon_client::mobs::Species::Wither {
|
||||
wither_heads.extend((0..3).map(|i| horizon_client::mobs::wither::head_pos(m, i)));
|
||||
}
|
||||
m.previous = m.position;
|
||||
m.previous_yaw = m.yaw;
|
||||
m.previous_body_yaw = m.body_yaw;
|
||||
@@ -3604,6 +3713,10 @@ impl Game<'_> {
|
||||
m.head_yaw += wrap_degrees(*yaw - m.head_yaw) / *steps as f32;
|
||||
*steps -= 1;
|
||||
}
|
||||
// Fox.tick: the head tilt and crouch follow the synched flags.
|
||||
if m.species() == horizon_client::mobs::Species::Fox {
|
||||
horizon_client::mobs::fox::client_tick(m);
|
||||
}
|
||||
// Chicken.aiStep: the wings follow the server's on-ground flag.
|
||||
if m.species() == horizon_client::mobs::Species::Chicken {
|
||||
horizon_client::mobs::farm::chicken_flap(m);
|
||||
@@ -3619,7 +3732,7 @@ impl Game<'_> {
|
||||
m.extra.main_hand.clone()
|
||||
};
|
||||
if let Some(item) = item {
|
||||
let eye = if m.extra.crouching { 1.27 } else { 1.62 };
|
||||
let eye = m.eye_height();
|
||||
let at = [m.position.x, m.position.y, m.position.z];
|
||||
using.push((
|
||||
item,
|
||||
@@ -3653,6 +3766,11 @@ impl Game<'_> {
|
||||
if m.species() == horizon_client::mobs::Species::Bat {
|
||||
horizon_client::mobs::bat::setup_animation_states(m);
|
||||
}
|
||||
// The animation states a server computes in its own `ai_step`.
|
||||
horizon_client::mobs::sync::client_tick(m, game_time);
|
||||
if m.species() == horizon_client::mobs::Species::Panda {
|
||||
horizon_client::mobs::panda::client_tick(m, thundering);
|
||||
}
|
||||
}
|
||||
for o in self.orbs.iter_mut().filter(|_| running) {
|
||||
o.previous = o.position;
|
||||
@@ -3677,10 +3795,24 @@ impl Game<'_> {
|
||||
d.tick(world, facts, collision, false, &self.res.motion().fluids);
|
||||
}
|
||||
head_lerps.retain(|_, (_, steps)| *steps > 0);
|
||||
// WitherBoss.aiStep runs on a client too: smoke around each head.
|
||||
let smoke = horizon_client::particles::ParticleOptions::simple(
|
||||
horizon_client::particles::ParticleType::Smoke,
|
||||
);
|
||||
for h in wither_heads {
|
||||
let r = self.particles.random();
|
||||
let at = [
|
||||
h[0] + r.next_gaussian() * 0.3,
|
||||
h[1] + r.next_gaussian() * 0.3,
|
||||
h[2] + r.next_gaussian() * 0.3,
|
||||
];
|
||||
self.particles.add(&smoke, at, [0.0; 3]);
|
||||
}
|
||||
for (item, ticks, at, eye, pitch, yaw) in using {
|
||||
self.remote_player_eating(&item, ticks, at, eye, pitch, yaw);
|
||||
}
|
||||
if running {
|
||||
self.guardian_bubbles();
|
||||
// The dragon: LivingEntity's interpolation, inside its aiStep.
|
||||
let dragon_lerp = self
|
||||
.dragon
|
||||
@@ -3935,6 +4067,9 @@ impl Game<'_> {
|
||||
if let Some(r) = self.remote.as_mut() {
|
||||
r.destroying = Some((hit.pos, face, 0.0));
|
||||
}
|
||||
// startDestroyBlock plays the first hit and crack itself; the
|
||||
// server's level events 2019 and 2020 do the rest.
|
||||
self.breaking_block_effects(hit.pos, hit.face, true);
|
||||
self.mining = Some(Mining {
|
||||
pos: hit.pos,
|
||||
progress: 0.0,
|
||||
@@ -3947,20 +4082,6 @@ impl Game<'_> {
|
||||
};
|
||||
let done = done + progress;
|
||||
let ticks = self.mining.as_ref().map_or(0, |m| m.ticks) + 1;
|
||||
if ticks % 4 == 1 {
|
||||
let c = [
|
||||
f64::from(pos[0]) + 0.5,
|
||||
f64::from(pos[1]) + 0.5,
|
||||
f64::from(pos[2]) + 0.5,
|
||||
];
|
||||
self.audio.play(
|
||||
&facts.sounds.hit,
|
||||
Some(c),
|
||||
(facts.sounds.volume + 1.0) / 8.0,
|
||||
facts.sounds.pitch * 0.5,
|
||||
);
|
||||
}
|
||||
self.crack_particle(pos, hit.face, state);
|
||||
if done >= 1.0 {
|
||||
let seq = self.next_sequence();
|
||||
self.predict_break(pos, state, seq);
|
||||
@@ -4128,6 +4249,7 @@ impl Game<'_> {
|
||||
}
|
||||
if reacts {
|
||||
self.predict_use(&changes, seq);
|
||||
self.open_command_block_if_editable((hit.pos[0], hit.pos[1], hit.pos[2]));
|
||||
} else if let Some(stack) = held.as_ref()
|
||||
&& self.predict_item_on_block(stack, &hit)
|
||||
{
|
||||
@@ -5234,6 +5356,11 @@ impl Game<'_> {
|
||||
batch.text(atlas, message, (w - tw) / 2.0, 85.0, u32::MAX, true);
|
||||
}
|
||||
}
|
||||
/// Shared flag 6 of an entity on a server's client (`None` without a server).
|
||||
pub(super) fn remote_glowing(&self, id: u32) -> Option<bool> {
|
||||
let r = self.remote.as_ref()?;
|
||||
Some(r.glowing.contains(&(id as i32)))
|
||||
}
|
||||
/// Status for `horizon-ctl status`.
|
||||
/// The other players' entities in the level and their names (from the
|
||||
/// player list).
|
||||
|
||||
@@ -4,7 +4,9 @@
|
||||
//! own simulation, which a client of a server doesn't run.
|
||||
use super::Game;
|
||||
use horizon_client::mobs::render::{PROFESSIONS, VILLAGER_TYPES};
|
||||
use horizon_client::mobs::{Mob, Species, Who, bat, bee, finalize, tame};
|
||||
use horizon_client::mobs::{
|
||||
Mob, Species, Who, armadillo, bat, bee, camel, creaking, finalize, panda, sniffer, sync, tame,
|
||||
};
|
||||
use horizon_client::particles::{ParticleOptions, ParticleType};
|
||||
use horizon_protocol::entity_data::{Particle, ParticleData, Value};
|
||||
use horizon_protocol::registry::{Builtin, Dynamic};
|
||||
@@ -17,10 +19,18 @@ fn apply(m: &mut Mob, values: &[(u8, i32, Value)], builtin: &Builtin, local_enti
|
||||
let baby = super::lan::baby_index(&m.kind.id);
|
||||
for (index, _, value) in values {
|
||||
match (*index, value) {
|
||||
// Entity.DATA_POSE: a player's or mannequin's box follows it.
|
||||
(6, Value::Id(pose))
|
||||
if matches!(&*m.kind.id, "minecraft:player" | "minecraft:mannequin") =>
|
||||
{
|
||||
m.extra.avatar_pose = horizon_client::pose::Pose::from_id(*pose);
|
||||
}
|
||||
// Entity.DATA_SHARED_FLAGS_ID's FLAG_ONFIRE: Entity.isOnFire on a
|
||||
// client, which draws the flames. A client doesn't count the
|
||||
// fire down: the flag goes off when the server's fire is out.
|
||||
(0, Value::Byte(flags)) => m.fire_ticks = i32::from(flags & 1),
|
||||
// Entity.DATA_POSE: the pose the species animates by.
|
||||
(6, Value::Id(pose)) => sync::set_pose(m, (*pose).clamp(0, 17) as u8),
|
||||
// LivingEntity.DATA_LIVING_ENTITY_FLAGS: using an item 1, in the
|
||||
// off hand 2. The flag starts and stops the use; the client ticks
|
||||
// the count itself (a bow's pull follows it).
|
||||
@@ -64,10 +74,23 @@ fn apply(m: &mut Mob, values: &[(u8, i32, Value)], builtin: &Builtin, local_enti
|
||||
Who::Mob(*target as u32)
|
||||
});
|
||||
}
|
||||
// Panda: the counters, the two genes and DATA_ID_FLAGS (after
|
||||
// AgeableMob's two entries).
|
||||
(Species::Panda, 18, Value::Int(n)) => panda::set_unhappy(m, *n),
|
||||
(Species::Panda, 19, Value::Int(n)) => panda::set_sneeze(m, *n),
|
||||
(Species::Panda, 20, Value::Int(n)) => panda::set_eat(m, *n),
|
||||
(Species::Panda, 21, Value::Byte(gene)) => m.extra.variant = (*gene).clamp(0, 6) as u8,
|
||||
(Species::Panda, 22, Value::Byte(gene)) => m.extra.variant2 = (*gene).clamp(0, 6) as u8,
|
||||
(Species::Panda, 23, Value::Byte(flags)) => panda::set_flags(m, *flags as u8),
|
||||
// PolarBear.DATA_STANDING_ID: the client animates the rearing.
|
||||
(Species::PolarBear, 18, Value::Bool(standing)) => m.extra.flag = *standing,
|
||||
// Bat.DATA_ID_FLAGS: resting in bit 0.
|
||||
(Species::Bat, 16, Value::Byte(flags)) => bat::set_resting(m, flags & 1 != 0),
|
||||
// Fox.DATA_TYPE_ID and DATA_FLAGS_ID.
|
||||
(Species::Fox, 18, Value::Int(variant)) => {
|
||||
m.extra.variant = (*variant).clamp(0, 1) as u8
|
||||
}
|
||||
(Species::Fox, 19, Value::Byte(flags)) => m.extra.ints[0] = i32::from(*flags as u8),
|
||||
// Sheep.DATA_WOOL_ID: the colour in the low nibble, 16 sheared.
|
||||
(Species::Sheep, 18, Value::Byte(wool)) => {
|
||||
m.extra.color = (*wool & 15) as u8;
|
||||
@@ -94,6 +117,55 @@ fn apply(m: &mut Mob, values: &[(u8, i32, Value)], builtin: &Builtin, local_enti
|
||||
(Species::Slime | Species::MagmaCube, 18, Value::Int(size)) => {
|
||||
m.extra.size = (*size).clamp(1, 127);
|
||||
}
|
||||
// Warden.CLIENT_ANGER_LEVEL: the heartbeat's pace.
|
||||
(Species::Warden, 16, Value::Int(anger)) => {
|
||||
if let Some(st) = m.extra.warden.as_mut() {
|
||||
st.client_anger = *anger;
|
||||
}
|
||||
}
|
||||
// Sniffer.DATA_STATE (after AgeableMob's two): the animation
|
||||
// follows it in `sync::client_tick`.
|
||||
(Species::Sniffer, 18, Value::Id(state)) => {
|
||||
sniffer::set_state(m, (*state).clamp(0, 6) as u8);
|
||||
}
|
||||
// Armadillo.ARMADILLO_STATE (after AgeableMob's two): the shell and
|
||||
// its animations follow it in `sync::client_tick`.
|
||||
(Species::Armadillo, 18, Value::Id(state)) => {
|
||||
armadillo::set_state(m, (*state).clamp(0, 3) as u8);
|
||||
}
|
||||
// Goat.DATA_HAS_LEFT_HORN and DATA_HAS_RIGHT_HORN: the model hides
|
||||
// a horn the goat lost.
|
||||
(Species::Goat, 19, Value::Bool(has)) => m.extra.lost_horns[0] = !*has,
|
||||
(Species::Goat, 20, Value::Bool(has)) => m.extra.lost_horns[1] = !*has,
|
||||
// Shulker.DATA_ATTACH_FACE_ID, DATA_PEEK_ID and DATA_COLOR_ID.
|
||||
(Species::Shulker, 16, Value::Id(face)) => m.extra.variant3 = (*face).clamp(0, 5) as u8,
|
||||
(Species::Shulker, 17, Value::Byte(peek)) => m.extra.counter = i32::from(*peek),
|
||||
(Species::Shulker, 18, Value::Byte(color)) => m.extra.color = *color as u8,
|
||||
// Camel.DASH and LAST_POSE_CHANGE_TICK (after AbstractHorse's
|
||||
// flags).
|
||||
(Species::Camel | Species::CamelHusk, 19, Value::Bool(dashing)) => {
|
||||
camel::set_dashing(m, *dashing);
|
||||
}
|
||||
(Species::Camel | Species::CamelHusk, 20, Value::Long(tick)) => {
|
||||
camel::set_synced_pose_tick(m, *tick);
|
||||
}
|
||||
// Vex.DATA_FLAGS_ID: charging in bit 0.
|
||||
(Species::Vex, 16, Value::Byte(flags)) => m.extra.flag = flags & 1 != 0,
|
||||
// Creaking.CAN_MOVE, IS_ACTIVE and IS_TEARING_DOWN.
|
||||
(Species::Creaking, 16, Value::Bool(can_move)) => m.extra.flag2 = !*can_move,
|
||||
(Species::Creaking, 17, Value::Bool(active)) => m.extra.flag = *active,
|
||||
(Species::Creaking, 18, Value::Bool(true)) => creaking::set_tearing_down(m),
|
||||
// Allay.DATA_DANCING and DATA_CAN_DUPLICATE (a cooldown running
|
||||
// is any non-zero `egg_time`).
|
||||
(Species::Allay, 16, Value::Bool(dancing)) => m.extra.flag = *dancing,
|
||||
(Species::Allay, 17, Value::Bool(can)) => m.extra.egg_time = i32::from(!*can),
|
||||
// Axolotl.DATA_PLAYING_DEAD (after DATA_VARIANT): the factors
|
||||
// ease in `sync::client_tick`.
|
||||
(Species::Axolotl, 19, Value::Bool(dead)) => m.extra.axolotl.playing_dead = *dead,
|
||||
// CopperGolem.COPPER_GOLEM_STATE (after DATA_WEATHER_STATE).
|
||||
(Species::CopperGolem, 17, Value::Id(state)) => {
|
||||
m.extra.variant3 = (*state).clamp(0, 4) as u8;
|
||||
}
|
||||
// AbstractHorse's flags take 18. Horse.DATA_ID_TYPE_VARIANT: the
|
||||
// colour in the low byte, the markings in the next.
|
||||
(Species::Horse, 19, Value::Int(v)) => {
|
||||
|
||||
@@ -78,7 +78,27 @@ fn remote_arrow_tick(p: &mut Projectile, w: &impl ProjectileWorld) {
|
||||
p.velocity.y -= 0.05;
|
||||
}
|
||||
|
||||
/// `AbstractHurtingProjectile.getTrailParticle`: smoke for every one of them
|
||||
/// but the wind charges (no trail) and the dragon's fireball (its own).
|
||||
pub(super) fn has_smoke_trail(kind: &ProjectileKind) -> bool {
|
||||
matches!(
|
||||
kind,
|
||||
ProjectileKind::SmallFireball
|
||||
| ProjectileKind::LargeFireball { .. }
|
||||
| ProjectileKind::WitherSkull { .. }
|
||||
)
|
||||
}
|
||||
|
||||
impl Game<'_> {
|
||||
/// `AbstractHurtingProjectile.createParticleTrail`: one smoke puff half a
|
||||
/// block above each position.
|
||||
pub(super) fn smoke_trails(&mut self, at: impl IntoIterator<Item = Vec3>) {
|
||||
let smoke = ParticleOptions::simple(ParticleType::Smoke);
|
||||
for p in at {
|
||||
self.particles.add(&smoke, [p.x, p.y + 0.5, p.z], [0.0; 3]);
|
||||
}
|
||||
}
|
||||
|
||||
/// `handleAddEntity` for these types; false when `kind` isn't one.
|
||||
pub(super) fn spawn_remote_projectile(
|
||||
&mut self,
|
||||
@@ -356,6 +376,14 @@ impl Game<'_> {
|
||||
}
|
||||
}
|
||||
self.creatures.projectiles = projectiles;
|
||||
let trails: Vec<Vec3> = self
|
||||
.creatures
|
||||
.projectiles
|
||||
.iter()
|
||||
.filter(|p| has_smoke_trail(&p.kind))
|
||||
.map(|p| p.position)
|
||||
.collect();
|
||||
self.smoke_trails(trails);
|
||||
for t in &mut self.tridents {
|
||||
let id = t.id;
|
||||
t.remote_step(|at| step(id, at));
|
||||
@@ -458,7 +486,7 @@ impl Game<'_> {
|
||||
];
|
||||
(c.particle.clone(), v)
|
||||
};
|
||||
self.particles.add(&options, at, velocity);
|
||||
self.particles.add_with(&options, at, velocity, false, true);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -245,6 +245,7 @@ impl Game<'_> {
|
||||
1.0,
|
||||
);
|
||||
}
|
||||
self.particles.particle_level = self.settings.particles;
|
||||
if let Some(e) = eye {
|
||||
self.particles.camera = e;
|
||||
}
|
||||
@@ -288,12 +289,14 @@ impl Game<'_> {
|
||||
e[2].floor() as i32,
|
||||
];
|
||||
animate_tick(self.res.animate(), &mut self.particles, &ctx, block);
|
||||
if rain > 0.0 {
|
||||
// ClientLevel.tickWeatherEffects: rain splashes (weather radius 10).
|
||||
// The Horizon Weather option off means no rain visuals at all.
|
||||
if rain > 0.0 && self.settings.horizon.weather {
|
||||
// ClientLevel.tickWeatherEffects: rain splashes within the weather radius.
|
||||
let mut random = LegacyRandom::new(self.world.game_time.wrapping_mul(312_987_231));
|
||||
let radius = 10;
|
||||
let radius = self.settings.weather_radius;
|
||||
let diameter = 2 * radius + 1;
|
||||
let count = (0.225 * (diameter * diameter) as f32 * rain * rain) as i32;
|
||||
let count = (0.225 * (diameter * diameter) as f32 * rain * rain) as i32
|
||||
/ if self.settings.particles == 1 { 2 } else { 1 };
|
||||
for _ in 0..count {
|
||||
let x = block[0] + random.next_int_bound(diameter).unwrap_or(0) - radius;
|
||||
let z = block[2] + random.next_int_bound(diameter).unwrap_or(0) - radius;
|
||||
@@ -305,6 +308,10 @@ impl Game<'_> {
|
||||
{
|
||||
continue;
|
||||
}
|
||||
// The Minimal option stops at the first spot that rains.
|
||||
if self.settings.particles >= 2 {
|
||||
break;
|
||||
}
|
||||
let below = top - 1;
|
||||
let (bx, bz) = (random.next_double(), random.next_double());
|
||||
let state = world.block(x, below, z);
|
||||
|
||||
@@ -1194,14 +1194,14 @@ impl Game<'_> {
|
||||
|
||||
/// `LightmapRenderStateExtractor`: the night vision effect's intensity
|
||||
/// (`GameRenderer.nightVisionScale`), or conduit power's water vision.
|
||||
pub(super) fn night_vision_intensity(&self) -> f32 {
|
||||
pub(super) fn night_vision_intensity(&self, partial: f32) -> f32 {
|
||||
use horizon_client::fog::FogType;
|
||||
if let Some(e) = self.potions.effects.get("night_vision") {
|
||||
// MobEffectInstance.endsWithin(200); -1 is infinite.
|
||||
return if e.duration < 0 || e.duration > 200 {
|
||||
1.0
|
||||
} else {
|
||||
0.7 + (e.duration as f32 * std::f32::consts::PI * 0.2).sin() * 0.3
|
||||
0.7 + ((e.duration as f32 - partial) * std::f32::consts::PI * 0.2).sin() * 0.3
|
||||
};
|
||||
}
|
||||
if self.potions.effects.get("conduit_power").is_some()
|
||||
@@ -1515,6 +1515,16 @@ impl Game<'_> {
|
||||
fn tick_entity_events(&mut self) {
|
||||
self.tick_player_sculk_events();
|
||||
self.tick_mob_sculk_events();
|
||||
// Dropped items are entities too: Entity.applyEffectsFromBlocks.
|
||||
let items: Vec<[f64; 3]> = self
|
||||
.dropped
|
||||
.iter()
|
||||
.filter(|i| i.on_ground)
|
||||
.map(|i| [i.position.x, i.position.y, i.position.z])
|
||||
.collect();
|
||||
for at in items {
|
||||
self.step_on_redstone_ore(at, false);
|
||||
}
|
||||
}
|
||||
|
||||
/// Entity.move emits on the authoritative server too, for each ServerPlayer.
|
||||
@@ -1530,6 +1540,9 @@ impl Game<'_> {
|
||||
});
|
||||
if let Some((at, on_ground, health, flying)) = player {
|
||||
let crouch = self.keys.contains(&KeyCode::ShiftLeft);
|
||||
if on_ground && !self.spectator() {
|
||||
self.step_on_redstone_ore(at, crouch);
|
||||
}
|
||||
let in_water = self.in_water_at(at);
|
||||
self.entity_motion_events(
|
||||
Who::Player,
|
||||
@@ -1548,6 +1561,26 @@ impl Game<'_> {
|
||||
}
|
||||
}
|
||||
|
||||
/// `RedStoneOreBlock.stepOn`: standing on the ore lights it, unless the
|
||||
/// entity is stepping carefully (`attack_block` only lights an unlit ore).
|
||||
fn step_on_redstone_ore(&mut self, at: [f64; 3], carefully: bool) {
|
||||
if carefully {
|
||||
return;
|
||||
}
|
||||
// getOnPosLegacy: 0.2 below the feet.
|
||||
let pos = [
|
||||
at[0].floor() as i32,
|
||||
(at[1] - 0.2).floor() as i32,
|
||||
at[2].floor() as i32,
|
||||
];
|
||||
if matches!(
|
||||
&*self.block_name(pos),
|
||||
"minecraft:redstone_ore" | "minecraft:deepslate_redstone_ore"
|
||||
) {
|
||||
let _ = self.attack_block(pos);
|
||||
}
|
||||
}
|
||||
|
||||
fn tick_mob_sculk_events(&mut self) {
|
||||
let snapshot: Vec<(u32, [f64; 3], bool, bool, Species, Option<i64>)> = self
|
||||
.mobs
|
||||
@@ -1565,6 +1598,9 @@ impl Game<'_> {
|
||||
})
|
||||
.collect();
|
||||
for (id, at, on_ground, in_water, species, damaged) in snapshot {
|
||||
if on_ground {
|
||||
self.step_on_redstone_ore(at, false);
|
||||
}
|
||||
let emits = !matches!(species, Species::Shulker);
|
||||
if emits {
|
||||
self.entity_motion_events(Who::Mob(id), at, on_ground, in_water, false, false);
|
||||
|
||||
@@ -546,6 +546,11 @@ impl Loading {
|
||||
(batch.width, batch.height),
|
||||
self.menu.modifiers.shift_key(),
|
||||
);
|
||||
title.press_rules(
|
||||
&gui.atlas,
|
||||
self.menu.cursor,
|
||||
(batch.width, batch.height),
|
||||
);
|
||||
self.menu_action(event_loop, action);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -21,7 +21,7 @@ pub(super) struct ThrownTrident {
|
||||
pub id: u32,
|
||||
pub(super) stack: ItemStack,
|
||||
pub(super) position: Vec3,
|
||||
previous: Vec3,
|
||||
pub(super) previous: Vec3,
|
||||
pub(super) velocity: Vec3,
|
||||
pub(super) yaw: f32,
|
||||
pub(super) pitch: f32,
|
||||
@@ -112,7 +112,7 @@ impl Game<'_> {
|
||||
}
|
||||
true
|
||||
}
|
||||
fn in_water_or_rain(&self) -> bool {
|
||||
fn in_water_or_rain(&mut self) -> bool {
|
||||
let Some(p) = &self.player else { return false };
|
||||
let (x, y, z) = (
|
||||
p.position.x.floor() as i32,
|
||||
@@ -121,8 +121,8 @@ impl Game<'_> {
|
||||
);
|
||||
let water = self.res.facts().get(self.world.block(x, y, z)).fluid
|
||||
== horizon_client::blocks::Fluid::Water;
|
||||
let sky = self.world.light(x, y + 1, z) >> 4 == 15;
|
||||
water || (self.world.weather.raining && sky)
|
||||
let (at, top) = (p.position, p.bounds().max_position().y);
|
||||
water || self.in_rain(at, top)
|
||||
}
|
||||
/// The living entities a spinning player can touch this tick, for
|
||||
/// `LivingEntity.checkAutoSpinAttack` (none when not spinning).
|
||||
|
||||
@@ -94,6 +94,8 @@ impl WorldRequest {
|
||||
level.difficulty = horizon_client::title::DIFFICULTIES[o.difficulty()].into();
|
||||
level.allow_commands = Some(o.commands(creative));
|
||||
horizon_client::save::write_world_gen_settings(&save_root, level.seed, o.world_gen())?;
|
||||
// WorldCreationUiState.getGameRules, written with the level.
|
||||
level.game_rules = o.rules.by_name();
|
||||
}
|
||||
// Worlds made without the Create New World screen (--world, the
|
||||
// control socket) take its default: commands on for Creative only.
|
||||
|
||||
+38
-30
@@ -1,8 +1,9 @@
|
||||
//! InWorldGameRulesScreen (AbstractGameRulesScreen), opened from World
|
||||
//! Options: the rules by category under a search box, a switch or a number
|
||||
//! box each, and Done sending the changed ones (ServerboundSetGameRulePacket).
|
||||
//! Leaving with changes asks first (ConfirmScreen).
|
||||
use horizon_client::{
|
||||
//! AbstractGameRulesScreen: the rules by category under a search box, a
|
||||
//! switch or a number box each. InWorldGameRulesScreen, opened from World
|
||||
//! Options, sends the changed ones on Done (ServerboundSetGameRulePacket) and
|
||||
//! asks before leaving with changes (ConfirmScreen); WorldCreationGameRulesScreen,
|
||||
//! opened from Create New World, hands them back and leaves without asking.
|
||||
use crate::{
|
||||
confirm_screen::Confirm,
|
||||
font::Font,
|
||||
gamerules::{self, GameRules, RULES, Rule, Value},
|
||||
@@ -32,14 +33,14 @@ enum Row {
|
||||
|
||||
/// What a click or Escape asks of World Options.
|
||||
#[derive(Debug, PartialEq)]
|
||||
pub(super) enum Exit {
|
||||
pub enum Exit {
|
||||
/// Done: the rules that changed.
|
||||
Apply(Vec<(&'static str, Value)>),
|
||||
/// Cancel, or discarding the changes.
|
||||
Discard,
|
||||
}
|
||||
|
||||
pub(super) struct RulesScreen {
|
||||
pub struct RulesScreen {
|
||||
search: EditBox,
|
||||
values: GameRules,
|
||||
initial: GameRules,
|
||||
@@ -49,6 +50,8 @@ pub(super) struct RulesScreen {
|
||||
scroll: ui::Scroll,
|
||||
/// ConfirmScreen "Game Rule Changes" in its place.
|
||||
confirm: bool,
|
||||
/// InWorldGameRulesScreen rather than WorldCreationGameRulesScreen.
|
||||
in_world: bool,
|
||||
}
|
||||
|
||||
fn name_key(r: &Rule) -> String {
|
||||
@@ -102,7 +105,15 @@ fn confirm(font: &Font, size: (f32, f32)) -> Confirm {
|
||||
}
|
||||
|
||||
impl RulesScreen {
|
||||
pub(super) fn new(rules: &GameRules) -> Self {
|
||||
/// InWorldGameRulesScreen over the world's rules.
|
||||
pub fn new(rules: &GameRules) -> Self {
|
||||
Self::build(rules, true)
|
||||
}
|
||||
/// WorldCreationGameRulesScreen over the new world's rules.
|
||||
pub fn for_creation(rules: &GameRules) -> Self {
|
||||
Self::build(rules, false)
|
||||
}
|
||||
fn build(rules: &GameRules, in_world: bool) -> Self {
|
||||
// setInitialFocus: the search box.
|
||||
let search = EditBox::new(0.0, 0.0, SEARCH_WIDTH, 15.0);
|
||||
let numbers = RULES
|
||||
@@ -123,6 +134,7 @@ impl RulesScreen {
|
||||
numbers,
|
||||
scroll: ui::Scroll::default(),
|
||||
confirm: false,
|
||||
in_world,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -197,9 +209,10 @@ impl RulesScreen {
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
/// InWorldGameRulesScreen.onClose: pending changes ask first.
|
||||
/// InWorldGameRulesScreen.onClose: pending changes ask first;
|
||||
/// WorldCreationGameRulesScreen.onClose discards them.
|
||||
fn close(&mut self) -> Option<Exit> {
|
||||
if self.changes().is_empty() {
|
||||
if !self.in_world || self.changes().is_empty() {
|
||||
Some(Exit::Discard)
|
||||
} else {
|
||||
self.confirm = true;
|
||||
@@ -207,7 +220,7 @@ impl RulesScreen {
|
||||
}
|
||||
}
|
||||
/// Escape: ConfirmScreen answers no; the rules close.
|
||||
pub(super) fn escape(&mut self) -> Option<Exit> {
|
||||
pub fn escape(&mut self) -> Option<Exit> {
|
||||
if self.confirm {
|
||||
self.confirm = false;
|
||||
None
|
||||
@@ -216,7 +229,7 @@ impl RulesScreen {
|
||||
}
|
||||
}
|
||||
/// The mouse wheel (AbstractSelectionList: half an entry a step).
|
||||
pub(super) fn wheel(&mut self, delta: f32, h: f32) {
|
||||
pub fn wheel(&mut self, delta: f32, h: f32) {
|
||||
if !self.confirm {
|
||||
let content = self.content();
|
||||
self.scroll
|
||||
@@ -224,20 +237,20 @@ impl RulesScreen {
|
||||
}
|
||||
}
|
||||
/// A press on the scrollbar holds it (AbstractSelectionList.updateScrolling).
|
||||
pub(super) fn press_scrollbar(&mut self, cursor: (f32, f32), (w, h): (f32, f32)) -> bool {
|
||||
pub fn press_scrollbar(&mut self, cursor: (f32, f32), (w, h): (f32, f32)) -> bool {
|
||||
let content = self.content();
|
||||
self.scroll
|
||||
.press(cursor, Self::bar(w), HEADER, h - FOOTER, content)
|
||||
}
|
||||
/// The pointer moved while the scrollbar is held.
|
||||
pub(super) fn drag(&mut self, y: f32, h: f32) {
|
||||
pub fn drag(&mut self, y: f32, h: f32) {
|
||||
if self.scroll.held() {
|
||||
let content = self.content();
|
||||
self.scroll.drag(y, HEADER, h - FOOTER, content);
|
||||
}
|
||||
}
|
||||
/// The button came up: the scrollbar lets go.
|
||||
pub(super) fn release(&mut self) {
|
||||
pub fn release(&mut self) {
|
||||
self.scroll.release();
|
||||
}
|
||||
fn focused(&mut self) -> Option<(Option<&'static str>, &mut EditBox)> {
|
||||
@@ -272,7 +285,7 @@ impl RulesScreen {
|
||||
}
|
||||
}
|
||||
/// Run an edit on the focused box, then its responder.
|
||||
pub(super) fn edit(&mut self, f: impl FnOnce(&mut EditBox)) {
|
||||
pub fn edit(&mut self, f: impl FnOnce(&mut EditBox)) {
|
||||
let Some((rule, b)) = self.focused() else {
|
||||
return;
|
||||
};
|
||||
@@ -282,12 +295,12 @@ impl RulesScreen {
|
||||
self.edited(rule);
|
||||
}
|
||||
}
|
||||
pub(super) fn typing(&self) -> bool {
|
||||
pub fn typing(&self) -> bool {
|
||||
!self.confirm && (self.search.focused || self.numbers.iter().any(|(_, b)| b.focused))
|
||||
}
|
||||
|
||||
/// A left click; Some when the screen closes.
|
||||
pub(super) fn click(
|
||||
pub fn click(
|
||||
&mut self,
|
||||
atlas: &HudAtlas,
|
||||
cursor: (f32, f32),
|
||||
@@ -355,12 +368,7 @@ impl RulesScreen {
|
||||
}
|
||||
}
|
||||
/// Whether a click lands on a button (for its sound).
|
||||
pub(super) fn clicks_button(
|
||||
&self,
|
||||
atlas: &HudAtlas,
|
||||
cursor: (f32, f32),
|
||||
size: (f32, f32),
|
||||
) -> bool {
|
||||
pub fn clicks_button(&self, atlas: &HudAtlas, cursor: (f32, f32), size: (f32, f32)) -> bool {
|
||||
let (w, h) = size;
|
||||
let on = |(x, y): (f32, f32)| ui::hovered(cursor, x, y, 150.0, 20.0);
|
||||
if self.confirm {
|
||||
@@ -377,9 +385,9 @@ impl RulesScreen {
|
||||
})
|
||||
}
|
||||
|
||||
pub(super) fn draw(&mut self, batch: &mut HudBatch, atlas: &HudAtlas, cursor: (f32, f32)) {
|
||||
pub fn draw(&mut self, batch: &mut HudBatch, atlas: &HudAtlas, cursor: (f32, f32)) {
|
||||
let (w, h) = (batch.width, batch.height);
|
||||
ui::menu_background(batch, atlas, 0.0, 0.0, w, h, true);
|
||||
ui::menu_background(batch, atlas, 0.0, 0.0, w, h, self.in_world);
|
||||
if self.confirm {
|
||||
let labels = [
|
||||
Text::translate("gui.yes", &[]),
|
||||
@@ -393,7 +401,7 @@ impl RulesScreen {
|
||||
self.scroll.clamp(HEADER, h - FOOTER, content);
|
||||
// RuleList: the in-world list background, its entries clipped to it,
|
||||
// then the separators and scrollbar.
|
||||
ui::list_background(batch, atlas, HEADER, h - FOOTER, true);
|
||||
ui::list_background(batch, atlas, HEADER, h - FOOTER, self.in_world);
|
||||
let first = batch.vertices.len();
|
||||
let hovered = self.hovered(rows.len(), cursor, (w, h));
|
||||
let (left, x) = (row_left(w), control_x(w));
|
||||
@@ -450,7 +458,7 @@ impl RulesScreen {
|
||||
}
|
||||
}
|
||||
batch.clip_since(first, 0.0, HEADER, w, h - FOOTER);
|
||||
ui::separators(batch, atlas, HEADER, h - FOOTER, true);
|
||||
ui::separators(batch, atlas, HEADER, h - FOOTER, self.in_world);
|
||||
ui::scrollbar(
|
||||
batch,
|
||||
atlas,
|
||||
@@ -526,8 +534,8 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn box_colours_are_vanillas() {
|
||||
assert_eq!(VALID, horizon_client::hud::rgba(224, 224, 224, 255));
|
||||
assert_eq!(INVALID, horizon_client::hud::rgba(255, 0, 0, 255));
|
||||
assert_eq!(VALID, crate::hud::rgba(224, 224, 224, 255));
|
||||
assert_eq!(INVALID, crate::hud::rgba(255, 0, 0, 255));
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -35,9 +35,13 @@ fn entity_shader() -> String {
|
||||
#[cfg(test)]
|
||||
mod crumbling_tests;
|
||||
mod entity_batch;
|
||||
mod entity_outline;
|
||||
mod weather;
|
||||
use entity_batch::EntityBatch;
|
||||
pub use entity_batch::{SHADOW_OVERLAY, TRANSLUCENT_PARTICLE_OVERLAY, WATER_MASK_OVERLAY};
|
||||
pub use entity_batch::{
|
||||
ENERGY_SWIRL_OVERLAY, SHADOW_OVERLAY, TRANSLUCENT_GLINT_ITEM_OVERLAY, TRANSLUCENT_ITEM_OVERLAY,
|
||||
TRANSLUCENT_PARTICLE_OVERLAY, WATER_MASK_OVERLAY,
|
||||
};
|
||||
|
||||
/// Window.getAppropriateLineWidth, in framebuffer pixels.
|
||||
pub fn outline_width(width: u32) -> f32 {
|
||||
@@ -247,6 +251,8 @@ pub struct Frame<'a> {
|
||||
pub outline: Vec<[f64; 6]>,
|
||||
/// `BlockOutlineRenderState.isTranslucent`: draw after translucent terrain.
|
||||
pub outline_after_terrain: bool,
|
||||
/// `BlockOutlineRenderState.highContrast`: a thick black outline under a bright one.
|
||||
pub outline_high_contrast: bool,
|
||||
/// `lines` with a colour and pixel width (`gizmos::camera_relative`):
|
||||
/// the fishing line and debug gizmos, depth tested, then gizmos drawn
|
||||
/// over everything.
|
||||
@@ -260,6 +266,9 @@ pub struct Frame<'a> {
|
||||
pub nether_lighting: bool,
|
||||
/// Camera-relative entity geometry textured from the shared atlas.
|
||||
pub entities: &'a [Vertex],
|
||||
/// The glowing entities' silhouettes in their team colours (`OutlineBufferSource`),
|
||||
/// outlined through walls after the hand.
|
||||
pub glowing: &'a [Vertex],
|
||||
/// Runs of `entities` by entity, while a shader pack draws them (`pack_draw::EntitySpan`).
|
||||
pub entity_spans: &'a [pack_draw::EntitySpan],
|
||||
pub weather: &'a [crate::weather::Vertex],
|
||||
@@ -377,6 +386,7 @@ pub struct Renderer {
|
||||
sky: wgpu::RenderPipeline,
|
||||
dark_disc: wgpu::RenderPipeline,
|
||||
lines: wgpu::RenderPipeline,
|
||||
lines_secondary: wgpu::RenderPipeline,
|
||||
/// Gizmo lines (`RenderTypes.linesTranslucentNoDepthWrite`) and the
|
||||
/// always-on-top ones.
|
||||
debug_lines: wgpu::RenderPipeline,
|
||||
@@ -391,6 +401,7 @@ pub struct Renderer {
|
||||
weather: weather::Weather,
|
||||
crumbling_pipeline: wgpu::RenderPipeline,
|
||||
shadow_blob_pipeline: wgpu::RenderPipeline,
|
||||
translucent_item_pipeline: wgpu::RenderPipeline,
|
||||
/// `RenderPipelines.WATER_MASK`: depth writes only, no colour.
|
||||
water_mask_pipeline: wgpu::RenderPipeline,
|
||||
cloud_pipeline: wgpu::RenderPipeline,
|
||||
@@ -400,6 +411,8 @@ pub struct Renderer {
|
||||
cloud_uniform: wgpu::Buffer,
|
||||
cloud_group: wgpu::BindGroup,
|
||||
additive_pipeline: wgpu::RenderPipeline,
|
||||
/// `RenderPipelines.ENERGY_SWIRL`: additive, unlit, the sprite repeated within itself.
|
||||
energy_swirl_pipeline: wgpu::RenderPipeline,
|
||||
lightning_pipeline: wgpu::RenderPipeline,
|
||||
lightning: lightning::Renderer,
|
||||
see_through_pipeline: wgpu::RenderPipeline,
|
||||
@@ -407,6 +420,8 @@ pub struct Renderer {
|
||||
celestial_buffer: wgpu::Buffer,
|
||||
end_sky: end_sky::EndSky,
|
||||
stars: stars::Stars,
|
||||
/// The glowing entities' outline (`LevelRenderer.entityOutlineTarget`).
|
||||
entity_outline: entity_outline::EntityOutline,
|
||||
panorama: panorama::Panorama,
|
||||
/// `RenderPipelines.WORLD_BORDER` and its buffers.
|
||||
border_pipeline: wgpu::RenderPipeline,
|
||||
@@ -1213,12 +1228,6 @@ impl Renderer {
|
||||
attributes: &instance_attributes,
|
||||
}),
|
||||
];
|
||||
let line_attributes = wgpu::vertex_attr_array![0=>Float32x3,1=>Float32x3];
|
||||
let line_buffers = [Some(wgpu::VertexBufferLayout {
|
||||
array_stride: 24,
|
||||
step_mode: wgpu::VertexStepMode::Instance,
|
||||
attributes: &line_attributes,
|
||||
})];
|
||||
let alpha_blend = wgpu::BlendState::ALPHA_BLENDING;
|
||||
let debug_line_attributes =
|
||||
wgpu::vertex_attr_array![0=>Float32x3,1=>Float32x3,2=>Unorm8x4,3=>Float32];
|
||||
@@ -1351,13 +1360,25 @@ impl Renderer {
|
||||
"Block outline",
|
||||
"vs_line",
|
||||
"fs_line",
|
||||
&line_buffers,
|
||||
&debug_line_buffers,
|
||||
wgpu::PrimitiveTopology::TriangleStrip,
|
||||
None,
|
||||
true,
|
||||
true,
|
||||
Some(alpha_blend),
|
||||
);
|
||||
// RenderTypes.secondaryBlockOutline: the black stroke under a high contrast outline.
|
||||
let lines_secondary = pipeline(
|
||||
"Block outline secondary",
|
||||
"vs_line",
|
||||
"fs_line",
|
||||
&debug_line_buffers,
|
||||
wgpu::PrimitiveTopology::TriangleStrip,
|
||||
None,
|
||||
false,
|
||||
true,
|
||||
Some(alpha_blend),
|
||||
);
|
||||
let debug_lines = pipeline(
|
||||
"Debug lines",
|
||||
"vs_debug_line",
|
||||
@@ -1531,7 +1552,7 @@ impl Renderer {
|
||||
});
|
||||
let outline = device.create_buffer(&wgpu::BufferDescriptor {
|
||||
label: Some("Outline"),
|
||||
size: 24 * MAX_OUTLINE_EDGES,
|
||||
size: 32 * 2 * MAX_OUTLINE_EDGES,
|
||||
usage: wgpu::BufferUsages::VERTEX | wgpu::BufferUsages::COPY_DST,
|
||||
mapped_at_creation: false,
|
||||
});
|
||||
@@ -1837,6 +1858,19 @@ impl Renderer {
|
||||
alpha_blend,
|
||||
false,
|
||||
);
|
||||
// RenderPipelines.ITEM_TRANSLUCENT: blended, depth tested. Drawn before
|
||||
// translucent terrain, it cannot write depth: the water behind a
|
||||
// glass item would fail the test and vanish (vanilla sorts these in
|
||||
// its order independent transparency pass instead).
|
||||
let translucent_item_pipeline = entity_pipeline(
|
||||
"Translucent items",
|
||||
None,
|
||||
false,
|
||||
wgpu::CompareFunction::GreaterEqual,
|
||||
Default::default(),
|
||||
alpha_blend,
|
||||
false,
|
||||
);
|
||||
let cloud_pipeline = entity_pipeline(
|
||||
"Clouds",
|
||||
Some(wgpu::Face::Back),
|
||||
@@ -1865,6 +1899,27 @@ impl Renderer {
|
||||
alpha_blend,
|
||||
false,
|
||||
);
|
||||
// RenderPipelines.ENERGY_SWIRL: BlendFunction.ADDITIVE (one, one), no culling; no depth
|
||||
// writes either: vanilla sorts the quads far to near, so every face passes the test.
|
||||
let one_one = wgpu::BlendComponent {
|
||||
src_factor: wgpu::BlendFactor::One,
|
||||
dst_factor: wgpu::BlendFactor::One,
|
||||
operation: wgpu::BlendOperation::Add,
|
||||
};
|
||||
let energy_swirl_pipeline = entity_pipeline_with(
|
||||
"Energy swirls",
|
||||
None,
|
||||
false,
|
||||
wgpu::CompareFunction::GreaterEqual,
|
||||
Default::default(),
|
||||
wgpu::BlendState {
|
||||
color: one_one,
|
||||
alpha: one_one,
|
||||
},
|
||||
false,
|
||||
"fs_energy_swirl",
|
||||
wgpu::ColorWrites::ALL,
|
||||
);
|
||||
let lightning_attributes = wgpu::vertex_attr_array![6=>Float32x3];
|
||||
let lightning_vertex = [
|
||||
entity_vertex[0].clone(),
|
||||
@@ -2316,6 +2371,11 @@ impl Renderer {
|
||||
let uploads = upload::Uploads::new(&device);
|
||||
let section_uploads = upload::Uploads::with_chunk(&device, SECTION_UPLOAD_CHUNK);
|
||||
let stars = stars::Stars::new(&device, config.format.remove_srgb_suffix());
|
||||
let entity_outline = entity_outline::EntityOutline::new(
|
||||
&device,
|
||||
(&entity_shader, &entity_layout, &entity_vertex),
|
||||
config.format.remove_srgb_suffix(),
|
||||
);
|
||||
let panorama = panorama::Panorama::new(
|
||||
&device,
|
||||
&queue,
|
||||
@@ -2361,6 +2421,7 @@ impl Renderer {
|
||||
sky,
|
||||
dark_disc,
|
||||
lines,
|
||||
lines_secondary,
|
||||
debug_lines,
|
||||
debug_lines_on_top,
|
||||
debug_line_buffer,
|
||||
@@ -2371,6 +2432,7 @@ impl Renderer {
|
||||
weather,
|
||||
crumbling_pipeline,
|
||||
shadow_blob_pipeline,
|
||||
translucent_item_pipeline,
|
||||
water_mask_pipeline,
|
||||
cloud_pipeline,
|
||||
cloud_mesh: Default::default(),
|
||||
@@ -2379,6 +2441,7 @@ impl Renderer {
|
||||
cloud_uniform,
|
||||
cloud_group,
|
||||
additive_pipeline,
|
||||
energy_swirl_pipeline,
|
||||
lightning_pipeline,
|
||||
lightning: lightning::Renderer::new(),
|
||||
see_through_pipeline,
|
||||
@@ -2386,6 +2449,7 @@ impl Renderer {
|
||||
celestial_buffer,
|
||||
end_sky: end_sky::EndSky::default(),
|
||||
stars,
|
||||
entity_outline,
|
||||
panorama,
|
||||
border_pipeline,
|
||||
border_group,
|
||||
@@ -2482,6 +2546,18 @@ impl Renderer {
|
||||
present_modes,
|
||||
})
|
||||
}
|
||||
/// LevelRenderer.submitBlockOutline: the buffer holds `edges` per stroke, the black
|
||||
/// secondary stroke first when high contrast is on.
|
||||
fn draw_outline(&self, pass: &mut wgpu::RenderPass<'_>, edges: u32, high_contrast: bool) {
|
||||
pass.set_vertex_buffer(0, self.outline.slice(..));
|
||||
if high_contrast {
|
||||
pass.set_pipeline(&self.lines_secondary);
|
||||
pass.draw(0..4, 0..edges);
|
||||
}
|
||||
pass.set_pipeline(&self.lines);
|
||||
let first = if high_contrast { edges } else { 0 };
|
||||
pass.draw(0..4, first..first + edges);
|
||||
}
|
||||
fn quad_indices(device: &wgpu::Device, quads: u32) -> wgpu::Buffer {
|
||||
let indices: Vec<u32> = (0..quads)
|
||||
.flat_map(|q| {
|
||||
@@ -3954,7 +4030,26 @@ impl Renderer {
|
||||
);
|
||||
let outline_edges = frame.outline.len().min(MAX_OUTLINE_EDGES as usize);
|
||||
if outline_edges > 0 {
|
||||
let lines = outline_vertices(&frame.outline[..outline_edges], camera);
|
||||
// The same per-line colour and pixel width layout as the gizmo lines. A high
|
||||
// contrast outline is two strokes: the black one first, the bright one after it.
|
||||
let edges = outline_vertices(&frame.outline[..outline_edges], camera);
|
||||
let width = outline_width(self.config.width);
|
||||
let stroke = |color: u32, width: f32| {
|
||||
edges.iter().map(move |e| {
|
||||
let mut line = [0.0; 8];
|
||||
line[..6].copy_from_slice(e);
|
||||
line[6] = f32::from_bits(color);
|
||||
line[7] = width;
|
||||
line
|
||||
})
|
||||
};
|
||||
let lines: Vec<[f32; 8]> = if frame.outline_high_contrast {
|
||||
stroke(0xff00_0000, 7.0)
|
||||
.chain(stroke(0xff57_ffe1, width))
|
||||
.collect()
|
||||
} else {
|
||||
stroke(0x6600_0000, width).collect()
|
||||
};
|
||||
self.uploads
|
||||
.buffer(&mut encoder, &self.outline, 0, bytemuck::cast_slice(&lines));
|
||||
}
|
||||
@@ -3979,8 +4074,10 @@ impl Renderer {
|
||||
crumbling_quads,
|
||||
translucent_crumbling_quads,
|
||||
water_mask_quads,
|
||||
swirl_quads,
|
||||
shadow_quads,
|
||||
translucent_particle_quads,
|
||||
translucent_item_quads,
|
||||
] = self
|
||||
.entity_batch
|
||||
.update(entities, frame.additive, frame.hand, frame.see_through);
|
||||
@@ -3989,8 +4086,10 @@ impl Renderer {
|
||||
+ crumbling_quads
|
||||
+ translucent_crumbling_quads
|
||||
+ water_mask_quads
|
||||
+ swirl_quads
|
||||
+ shadow_quads
|
||||
+ translucent_particle_quads;
|
||||
+ translucent_particle_quads
|
||||
+ translucent_item_quads;
|
||||
let cloud_quads = if let Some(input) = &frame.clouds {
|
||||
let (rebuilt, uniform) =
|
||||
crate::spans::time("clouds", || self.cloud_mesh.prepare(input));
|
||||
@@ -4051,6 +4150,13 @@ impl Renderer {
|
||||
bytemuck::cast_slice(all_entities),
|
||||
);
|
||||
}
|
||||
self.entity_outline.prepare(
|
||||
&self.device,
|
||||
&mut self.uploads,
|
||||
&mut encoder,
|
||||
frame.glowing,
|
||||
[self.config.width, self.config.height],
|
||||
);
|
||||
if let Some((rotation, brightness)) = frame.stars {
|
||||
self.stars.prepare(
|
||||
&mut self.uploads,
|
||||
@@ -4078,7 +4184,8 @@ impl Renderer {
|
||||
} else {
|
||||
lightning::QUADS
|
||||
})
|
||||
.max(weather_quads);
|
||||
.max(weather_quads)
|
||||
.max(frame.glowing.len() as u32 / 4);
|
||||
if max_quads > self.index_quads {
|
||||
self.index_quads = max_quads.next_power_of_two();
|
||||
self.indices = Self::quad_indices(&self.device, self.index_quads);
|
||||
@@ -4513,6 +4620,17 @@ impl Renderer {
|
||||
pass.draw_indexed(0..entity_quads * 6, 0, 0..1);
|
||||
pass.set_bind_group(0, &self.group, &[]);
|
||||
}
|
||||
if translucent_item_quads > 0 {
|
||||
pass.set_pipeline(&self.translucent_item_pipeline);
|
||||
pass.set_bind_group(0, &self.entity_group, &[]);
|
||||
pass.set_vertex_buffer(0, self.entity_buffer.slice(..));
|
||||
pass.draw_indexed(
|
||||
0..translucent_item_quads * 6,
|
||||
((world_quads - translucent_item_quads) * 4) as i32,
|
||||
0..1,
|
||||
);
|
||||
pass.set_bind_group(0, &self.group, &[]);
|
||||
}
|
||||
if !pack_draws_weather {
|
||||
self.weather.draw(&mut pass, &self.entity_group);
|
||||
}
|
||||
@@ -4530,7 +4648,11 @@ impl Renderer {
|
||||
pass.set_vertex_buffer(0, self.entity_buffer.slice(..));
|
||||
pass.draw_indexed(
|
||||
0..shadow_quads * 6,
|
||||
((world_quads - translucent_particle_quads - shadow_quads) * 4) as i32,
|
||||
((world_quads
|
||||
- translucent_item_quads
|
||||
- translucent_particle_quads
|
||||
- shadow_quads)
|
||||
* 4) as i32,
|
||||
0..1,
|
||||
);
|
||||
pass.set_bind_group(0, &self.group, &[]);
|
||||
@@ -4554,6 +4676,21 @@ impl Renderer {
|
||||
self.lightning.draw(&mut pass, &frame.lightning);
|
||||
pass.set_bind_group(0, &self.group, &[]);
|
||||
}
|
||||
if swirl_quads > 0 {
|
||||
pass.set_pipeline(&self.energy_swirl_pipeline);
|
||||
pass.set_bind_group(0, &self.entity_group, &[]);
|
||||
pass.set_vertex_buffer(0, self.entity_buffer.slice(..));
|
||||
pass.draw_indexed(
|
||||
0..swirl_quads * 6,
|
||||
((entity_quads
|
||||
+ crumbling_quads
|
||||
+ translucent_crumbling_quads
|
||||
+ water_mask_quads)
|
||||
* 4) as i32,
|
||||
0..1,
|
||||
);
|
||||
pass.set_bind_group(0, &self.group, &[]);
|
||||
}
|
||||
if additive_quads > 0 {
|
||||
pass.set_pipeline(&self.additive_pipeline);
|
||||
pass.set_bind_group(0, &self.entity_group, &[]);
|
||||
@@ -4562,9 +4699,7 @@ impl Renderer {
|
||||
pass.set_bind_group(0, &self.group, &[]);
|
||||
}
|
||||
if outline_edges > 0 && !frame.outline_after_terrain {
|
||||
pass.set_pipeline(&self.lines);
|
||||
pass.set_vertex_buffer(0, self.outline.slice(..));
|
||||
pass.draw(0..4, 0..outline_edges as u32);
|
||||
self.draw_outline(&mut pass, outline_edges as u32, frame.outline_high_contrast);
|
||||
}
|
||||
if debug_depth > 0 {
|
||||
pass.set_pipeline(&self.debug_lines);
|
||||
@@ -4611,7 +4746,8 @@ impl Renderer {
|
||||
pass.set_vertex_buffer(0, self.entity_buffer.slice(..));
|
||||
pass.draw_indexed(
|
||||
0..translucent_particle_quads * 6,
|
||||
((world_quads - translucent_particle_quads) * 4) as i32,
|
||||
((world_quads - translucent_item_quads - translucent_particle_quads) * 4)
|
||||
as i32,
|
||||
0..1,
|
||||
);
|
||||
pass.set_bind_group(0, &self.group, &[]);
|
||||
@@ -4619,9 +4755,7 @@ impl Renderer {
|
||||
// LevelRenderer.executeClassicTransparency: translucent targets'
|
||||
// outlines follow terrain, so its blending cannot wash them out.
|
||||
if outline_edges > 0 && frame.outline_after_terrain {
|
||||
pass.set_pipeline(&self.lines);
|
||||
pass.set_vertex_buffer(0, self.outline.slice(..));
|
||||
pass.draw(0..4, 0..outline_edges as u32);
|
||||
self.draw_outline(&mut pass, outline_edges as u32, frame.outline_high_contrast);
|
||||
}
|
||||
pass.set_index_buffer(self.indices.slice(..), wgpu::IndexFormat::Uint32);
|
||||
// LevelRenderer: the world border after translucent terrain and weather.
|
||||
@@ -4808,6 +4942,13 @@ impl Renderer {
|
||||
0..1,
|
||||
);
|
||||
}
|
||||
// GameRenderer.render: LevelRenderer.blitEntityOutline after the level and the hand.
|
||||
self.entity_outline.draw(
|
||||
&mut encoder,
|
||||
&view_target,
|
||||
&self.entity_group,
|
||||
&self.indices,
|
||||
);
|
||||
if !frame.screen.is_empty() {
|
||||
let attachments = [Some(wgpu::RenderPassColorAttachment {
|
||||
view: &view_target,
|
||||
@@ -5240,7 +5381,7 @@ mod tests {
|
||||
let mut batch = super::EntityBatch::default();
|
||||
let counts = batch.update(&entities, &quad(3, 5), &quad(0, 6), &quad(0, 8));
|
||||
let vertices = &batch.combined;
|
||||
assert_eq!(counts, [4, 1, 1, 0, 1, 1]);
|
||||
assert_eq!(counts, [4, 1, 1, 0, 0, 1, 1, 0]);
|
||||
assert_eq!(
|
||||
vertices
|
||||
.as_chunks::<4>()
|
||||
|
||||
@@ -3,16 +3,27 @@ use super::Vertex;
|
||||
|
||||
/// Entity materials by a quad's overlay (`Vertex::layer`), in draw order:
|
||||
/// ordinary entities, breaking cracks (11), translucent breaking cracks (12),
|
||||
/// the boat water mask (13, `RenderPipelines.WATER_MASK`), entity shadows
|
||||
/// (14, `RenderPipelines.ENTITY_SHADOW`) and translucent-layer particles (15,
|
||||
/// drawn after translucent terrain: `SubmitNodeCollection.submitQuadParticleGroup`).
|
||||
const MATERIALS: usize = 6;
|
||||
/// the boat water mask (13, `RenderPipelines.WATER_MASK`), energy swirls
|
||||
/// (34, `RenderPipelines.ENERGY_SWIRL`), entity shadows (14,
|
||||
/// `RenderPipelines.ENTITY_SHADOW`), translucent-layer particles (15, drawn
|
||||
/// after translucent terrain: `SubmitNodeCollection.submitQuadParticleGroup`)
|
||||
/// and translucent item quads (32, drawn without depth writes).
|
||||
const MATERIALS: usize = 8;
|
||||
/// The overlay of depth-only quads that hide water (`RenderTypes.waterMask`).
|
||||
pub const WATER_MASK_OVERLAY: u32 = 13;
|
||||
/// The overlay of energy swirl quads (`RenderTypes.energySwirl`: the charged creeper's and the
|
||||
/// powered wither's armour). The light bytes carry the sprite's origin and the colour bytes its
|
||||
/// size (`breaking::stage_light`), the UV the scrolled position in the sprite.
|
||||
pub const ENERGY_SWIRL_OVERLAY: u32 = 34;
|
||||
/// The overlay of entity shadow quads (`RenderTypes.entityShadow`).
|
||||
pub const SHADOW_OVERLAY: u32 = 14;
|
||||
/// The overlay of translucent-layer particles (`SingleQuadParticle.Layer.translucent`).
|
||||
pub const TRANSLUCENT_PARTICLE_OVERLAY: u32 = 15;
|
||||
/// The overlay of item quads with translucent pixels (`Sheets.translucentItemSheet`,
|
||||
/// `RenderPipelines.ITEM_TRANSLUCENT`); past the creeper swell range (16 to 31).
|
||||
pub const TRANSLUCENT_ITEM_OVERLAY: u32 = 32;
|
||||
/// A translucent item with an enchantment glint.
|
||||
pub const TRANSLUCENT_GLINT_ITEM_OVERLAY: u32 = 33;
|
||||
|
||||
#[derive(Default)]
|
||||
pub(super) struct EntityBatch {
|
||||
@@ -43,8 +54,10 @@ impl EntityBatch {
|
||||
11 => 1,
|
||||
12 => 2,
|
||||
WATER_MASK_OVERLAY => 3,
|
||||
SHADOW_OVERLAY => 4,
|
||||
TRANSLUCENT_PARTICLE_OVERLAY => 5,
|
||||
ENERGY_SWIRL_OVERLAY => 4,
|
||||
SHADOW_OVERLAY => 5,
|
||||
TRANSLUCENT_PARTICLE_OVERLAY => 6,
|
||||
TRANSLUCENT_ITEM_OVERLAY | TRANSLUCENT_GLINT_ITEM_OVERLAY => 7,
|
||||
_ => 0,
|
||||
};
|
||||
self.buckets[material][counts[material]] = index;
|
||||
@@ -96,12 +109,13 @@ mod tests {
|
||||
) -> (Vec<Vertex>, [u32; MATERIALS]) {
|
||||
let mut combined = Vec::with_capacity(entities.len() + additive.len() + hand.len());
|
||||
let mut counts = [0; MATERIALS];
|
||||
for (i, layer) in [0, 11, 12, 13, 14, 15].into_iter().enumerate() {
|
||||
for (i, layer) in [0, 11, 12, 13, 34, 14, 15].into_iter().enumerate() {
|
||||
for quad in entities.as_chunks::<4>().0 {
|
||||
let material = match quad[0].layer {
|
||||
11 => 11,
|
||||
12 => 12,
|
||||
13 => 13,
|
||||
34 => 34,
|
||||
14 => 14,
|
||||
15 => 15,
|
||||
_ => 0,
|
||||
|
||||
@@ -0,0 +1,311 @@
|
||||
//! The entity outline (`LevelRenderer.entityOutlineTarget`): the glowing entities'
|
||||
//! silhouettes in their team colours, drawn without depth so walls do not hide them
|
||||
//! (`RenderTypes.outline`), the `entity_outline` post chain over them (`entity_sobel`, then
|
||||
//! `entity_outline_box_blur` across and up), and `LevelRenderer.blitEntityOutline` onto
|
||||
//! the frame after the hand.
|
||||
use super::Vertex;
|
||||
|
||||
/// `entityOutlineTarget`'s `GpuFormat.RGBA8_UNORM`.
|
||||
const FORMAT: wgpu::TextureFormat = wgpu::TextureFormat::Rgba8Unorm;
|
||||
|
||||
struct Targets {
|
||||
size: [u32; 2],
|
||||
/// `entityOutlineTarget`.
|
||||
outline: wgpu::TextureView,
|
||||
/// The chain's `swap` target.
|
||||
swap: wgpu::TextureView,
|
||||
/// What each pass reads: the outline (nearest), swap (bilinear), the outline
|
||||
/// (bilinear), swap (nearest).
|
||||
groups: [wgpu::BindGroup; 4],
|
||||
}
|
||||
|
||||
pub(super) struct EntityOutline {
|
||||
silhouette: wgpu::RenderPipeline,
|
||||
/// Sobel, blur across, blur up (into `FORMAT`) and the blended blit (onto the frame).
|
||||
passes: [wgpu::RenderPipeline; 4],
|
||||
layout: wgpu::BindGroupLayout,
|
||||
nearest: wgpu::Sampler,
|
||||
bilinear: wgpu::Sampler,
|
||||
vertices: wgpu::Buffer,
|
||||
capacity: u64,
|
||||
quads: u32,
|
||||
targets: Option<Targets>,
|
||||
}
|
||||
|
||||
impl EntityOutline {
|
||||
/// `entity` draws the silhouettes with the entity shader's vertex stage and
|
||||
/// `fs_outline`; `format` is the frame's.
|
||||
pub(super) fn new(
|
||||
device: &wgpu::Device,
|
||||
entity: (
|
||||
&wgpu::ShaderModule,
|
||||
&wgpu::PipelineLayout,
|
||||
&[Option<wgpu::VertexBufferLayout>],
|
||||
),
|
||||
format: wgpu::TextureFormat,
|
||||
) -> Self {
|
||||
let (entity_shader, entity_layout, entity_vertex) = entity;
|
||||
// RenderPipelines.OUTLINE_NO_CULL: no depth test, the colour replaced.
|
||||
let silhouette = device.create_render_pipeline(&wgpu::RenderPipelineDescriptor {
|
||||
label: Some("Entity outline silhouettes"),
|
||||
layout: Some(entity_layout),
|
||||
vertex: wgpu::VertexState {
|
||||
module: entity_shader,
|
||||
entry_point: Some("vs_entity"),
|
||||
compilation_options: Default::default(),
|
||||
buffers: entity_vertex,
|
||||
},
|
||||
primitive: Default::default(),
|
||||
depth_stencil: None,
|
||||
multisample: Default::default(),
|
||||
fragment: Some(wgpu::FragmentState {
|
||||
module: entity_shader,
|
||||
entry_point: Some("fs_outline"),
|
||||
compilation_options: Default::default(),
|
||||
targets: &[Some(FORMAT.into())],
|
||||
}),
|
||||
multiview_mask: None,
|
||||
cache: None,
|
||||
});
|
||||
let shader = device.create_shader_module(wgpu::ShaderModuleDescriptor {
|
||||
label: Some("Entity outline"),
|
||||
source: wgpu::ShaderSource::Wgsl(include_str!("entity_outline.wgsl").into()),
|
||||
});
|
||||
let layout = device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
|
||||
label: Some("Entity outline bindings"),
|
||||
entries: &[
|
||||
wgpu::BindGroupLayoutEntry {
|
||||
binding: 0,
|
||||
visibility: wgpu::ShaderStages::FRAGMENT,
|
||||
ty: wgpu::BindingType::Texture {
|
||||
sample_type: wgpu::TextureSampleType::Float { filterable: true },
|
||||
view_dimension: wgpu::TextureViewDimension::D2,
|
||||
multisampled: false,
|
||||
},
|
||||
count: None,
|
||||
},
|
||||
wgpu::BindGroupLayoutEntry {
|
||||
binding: 1,
|
||||
visibility: wgpu::ShaderStages::FRAGMENT,
|
||||
ty: wgpu::BindingType::Sampler(wgpu::SamplerBindingType::Filtering),
|
||||
count: None,
|
||||
},
|
||||
],
|
||||
});
|
||||
let pipeline_layout = device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor {
|
||||
label: Some("Entity outline"),
|
||||
bind_group_layouts: &[Some(&layout)],
|
||||
..Default::default()
|
||||
});
|
||||
let pass = |entry: &str, target: wgpu::ColorTargetState| {
|
||||
device.create_render_pipeline(&wgpu::RenderPipelineDescriptor {
|
||||
label: Some(entry),
|
||||
layout: Some(&pipeline_layout),
|
||||
vertex: wgpu::VertexState {
|
||||
module: &shader,
|
||||
entry_point: Some("vs_quad"),
|
||||
compilation_options: Default::default(),
|
||||
buffers: &[],
|
||||
},
|
||||
primitive: Default::default(),
|
||||
depth_stencil: None,
|
||||
multisample: Default::default(),
|
||||
fragment: Some(wgpu::FragmentState {
|
||||
module: &shader,
|
||||
entry_point: Some(entry),
|
||||
compilation_options: Default::default(),
|
||||
targets: &[Some(target)],
|
||||
}),
|
||||
multiview_mask: None,
|
||||
cache: None,
|
||||
})
|
||||
};
|
||||
// BlendFunction.ENTITY_OUTLINE_BLIT over the colour only (write mask 7).
|
||||
let blit = wgpu::ColorTargetState {
|
||||
format,
|
||||
blend: Some(wgpu::BlendState {
|
||||
color: wgpu::BlendComponent {
|
||||
src_factor: wgpu::BlendFactor::SrcAlpha,
|
||||
dst_factor: wgpu::BlendFactor::OneMinusSrcAlpha,
|
||||
operation: wgpu::BlendOperation::Add,
|
||||
},
|
||||
alpha: wgpu::BlendComponent {
|
||||
src_factor: wgpu::BlendFactor::Zero,
|
||||
dst_factor: wgpu::BlendFactor::One,
|
||||
operation: wgpu::BlendOperation::Add,
|
||||
},
|
||||
}),
|
||||
write_mask: wgpu::ColorWrites::COLOR,
|
||||
};
|
||||
let sampler = |label, filter| {
|
||||
device.create_sampler(&wgpu::SamplerDescriptor {
|
||||
label: Some(label),
|
||||
mag_filter: filter,
|
||||
min_filter: filter,
|
||||
..Default::default()
|
||||
})
|
||||
};
|
||||
Self {
|
||||
silhouette,
|
||||
passes: [
|
||||
pass("fs_sobel", FORMAT.into()),
|
||||
pass("fs_blur_x", FORMAT.into()),
|
||||
pass("fs_blur_y", FORMAT.into()),
|
||||
pass("fs_blit", blit),
|
||||
],
|
||||
layout,
|
||||
nearest: sampler("Entity outline nearest", wgpu::FilterMode::Nearest),
|
||||
bilinear: sampler("Entity outline bilinear", wgpu::FilterMode::Linear),
|
||||
vertices: Self::vertex_buffer(device, 256),
|
||||
capacity: 256,
|
||||
quads: 0,
|
||||
targets: None,
|
||||
}
|
||||
}
|
||||
|
||||
fn vertex_buffer(device: &wgpu::Device, capacity: u64) -> wgpu::Buffer {
|
||||
device.create_buffer(&wgpu::BufferDescriptor {
|
||||
label: Some("Entity outline vertices"),
|
||||
size: capacity * std::mem::size_of::<Vertex>() as u64,
|
||||
usage: wgpu::BufferUsages::VERTEX | wgpu::BufferUsages::COPY_DST,
|
||||
mapped_at_creation: false,
|
||||
})
|
||||
}
|
||||
|
||||
/// Uploads this frame's silhouettes (`LevelRenderer.currentFrameRendersEntityOutline`
|
||||
/// when there are any) and sizes the targets like the window.
|
||||
pub(super) fn prepare(
|
||||
&mut self,
|
||||
device: &wgpu::Device,
|
||||
uploads: &mut super::upload::Uploads,
|
||||
encoder: &mut wgpu::CommandEncoder,
|
||||
silhouettes: &[Vertex],
|
||||
size: [u32; 2],
|
||||
) {
|
||||
self.quads = (silhouettes.len() / 4) as u32;
|
||||
if self.quads == 0 {
|
||||
return;
|
||||
}
|
||||
let vertices = silhouettes.len() as u64;
|
||||
if vertices > self.capacity {
|
||||
self.capacity = vertices.next_power_of_two();
|
||||
self.vertices = Self::vertex_buffer(device, self.capacity);
|
||||
}
|
||||
uploads.buffer(
|
||||
encoder,
|
||||
&self.vertices,
|
||||
0,
|
||||
bytemuck::cast_slice(silhouettes),
|
||||
);
|
||||
if self.targets.as_ref().is_some_and(|t| t.size == size) {
|
||||
return;
|
||||
}
|
||||
let texture = |label| {
|
||||
device
|
||||
.create_texture(&wgpu::TextureDescriptor {
|
||||
label: Some(label),
|
||||
size: wgpu::Extent3d {
|
||||
width: size[0],
|
||||
height: size[1],
|
||||
depth_or_array_layers: 1,
|
||||
},
|
||||
mip_level_count: 1,
|
||||
sample_count: 1,
|
||||
dimension: wgpu::TextureDimension::D2,
|
||||
format: FORMAT,
|
||||
usage: wgpu::TextureUsages::RENDER_ATTACHMENT
|
||||
| wgpu::TextureUsages::TEXTURE_BINDING,
|
||||
view_formats: &[],
|
||||
})
|
||||
.create_view(&Default::default())
|
||||
};
|
||||
let outline = texture("Entity outline");
|
||||
let swap = texture("Entity outline swap");
|
||||
let group = |view: &wgpu::TextureView, sampler: &wgpu::Sampler| {
|
||||
device.create_bind_group(&wgpu::BindGroupDescriptor {
|
||||
label: Some("Entity outline pass"),
|
||||
layout: &self.layout,
|
||||
entries: &[
|
||||
wgpu::BindGroupEntry {
|
||||
binding: 0,
|
||||
resource: wgpu::BindingResource::TextureView(view),
|
||||
},
|
||||
wgpu::BindGroupEntry {
|
||||
binding: 1,
|
||||
resource: wgpu::BindingResource::Sampler(sampler),
|
||||
},
|
||||
],
|
||||
})
|
||||
};
|
||||
let groups = [
|
||||
group(&outline, &self.nearest),
|
||||
group(&swap, &self.bilinear),
|
||||
group(&outline, &self.bilinear),
|
||||
group(&swap, &self.nearest),
|
||||
];
|
||||
self.targets = Some(Targets {
|
||||
size,
|
||||
outline,
|
||||
swap,
|
||||
groups,
|
||||
});
|
||||
}
|
||||
|
||||
/// `LevelRenderer.executeOutline`, the post chain and `blitEntityOutline` onto `frame`.
|
||||
pub(super) fn draw(
|
||||
&self,
|
||||
encoder: &mut wgpu::CommandEncoder,
|
||||
frame: &wgpu::TextureView,
|
||||
entity_group: &wgpu::BindGroup,
|
||||
indices: &wgpu::Buffer,
|
||||
) {
|
||||
let Some(t) = self.targets.as_ref().filter(|_| self.quads > 0) else {
|
||||
return;
|
||||
};
|
||||
let clear = wgpu::LoadOp::Clear(wgpu::Color::TRANSPARENT);
|
||||
{
|
||||
let mut pass = begin(encoder, "Entity outline", &t.outline, clear);
|
||||
pass.set_pipeline(&self.silhouette);
|
||||
pass.set_bind_group(0, entity_group, &[]);
|
||||
pass.set_index_buffer(indices.slice(..), wgpu::IndexFormat::Uint32);
|
||||
pass.set_vertex_buffer(0, self.vertices.slice(..));
|
||||
pass.draw_indexed(0..self.quads * 6, 0, 0..1);
|
||||
}
|
||||
// post_effect/entity_outline.json; its last pass only copies swap back to the
|
||||
// outline target, so the blit reads swap.
|
||||
let steps = [
|
||||
("Entity outline sobel", &t.swap, clear),
|
||||
("Entity outline blur across", &t.outline, clear),
|
||||
("Entity outline blur up", &t.swap, clear),
|
||||
("Entity outline blit", frame, wgpu::LoadOp::Load),
|
||||
];
|
||||
for (i, (label, view, load)) in steps.into_iter().enumerate() {
|
||||
let mut pass = begin(encoder, label, view, load);
|
||||
pass.set_pipeline(&self.passes[i]);
|
||||
pass.set_bind_group(0, &t.groups[i], &[]);
|
||||
pass.draw(0..3, 0..1);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn begin<'e>(
|
||||
encoder: &'e mut wgpu::CommandEncoder,
|
||||
label: &str,
|
||||
view: &wgpu::TextureView,
|
||||
load: wgpu::LoadOp<wgpu::Color>,
|
||||
) -> wgpu::RenderPass<'e> {
|
||||
let attachments = [Some(wgpu::RenderPassColorAttachment {
|
||||
view,
|
||||
resolve_target: None,
|
||||
depth_slice: None,
|
||||
ops: wgpu::Operations {
|
||||
load,
|
||||
store: wgpu::StoreOp::Store,
|
||||
},
|
||||
})];
|
||||
encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
|
||||
label: Some(label),
|
||||
color_attachments: &attachments,
|
||||
..Default::default()
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,60 @@
|
||||
// The entity_outline post chain (post_effect/entity_outline.json) over the silhouettes of
|
||||
// the glowing entities, then RenderTarget.blitAndBlendToTexture onto the frame.
|
||||
@group(0) @binding(0) var source: texture_2d<f32>;
|
||||
@group(0) @binding(1) var source_sampler: sampler;
|
||||
|
||||
struct PostOut {
|
||||
@builtin(position) clip: vec4<f32>,
|
||||
@location(0) uv: vec2<f32>,
|
||||
};
|
||||
|
||||
// screenquad.vsh, with v running down the rows as wgpu's textures do.
|
||||
@vertex fn vs_quad(@builtin(vertex_index) i: u32) -> PostOut {
|
||||
let p = vec2<f32>(f32((i << 1u) & 2u), f32(i & 2u));
|
||||
var out: PostOut;
|
||||
out.clip = vec4(p * 2.0 - 1.0, 0.0, 1.0);
|
||||
out.uv = vec2(p.x, 1.0 - p.y);
|
||||
return out;
|
||||
}
|
||||
|
||||
fn tap(uv: vec2<f32>) -> vec4<f32> {
|
||||
return textureSampleLevel(source, source_sampler, uv, 0.0);
|
||||
}
|
||||
|
||||
// entity_sobel.fsh: alpha marks where coverage changes, the colour is the mean of the
|
||||
// covered texels around.
|
||||
@fragment fn fs_sobel(in: PostOut) -> @location(0) vec4<f32> {
|
||||
let texel = 1.0 / vec2<f32>(textureDimensions(source));
|
||||
let center = tap(in.uv);
|
||||
let left = tap(in.uv - vec2(texel.x, 0.0));
|
||||
let right = tap(in.uv + vec2(texel.x, 0.0));
|
||||
let up = tap(in.uv - vec2(0.0, texel.y));
|
||||
let down = tap(in.uv + vec2(0.0, texel.y));
|
||||
let total = clamp(abs(center.a - left.a) + abs(center.a - right.a)
|
||||
+ abs(center.a - up.a) + abs(center.a - down.a), 0.0, 1.0);
|
||||
let color = center.rgb * center.a + left.rgb * left.a + right.rgb * right.a
|
||||
+ up.rgb * up.a + down.rgb * down.a;
|
||||
return vec4(color * 0.2, total);
|
||||
}
|
||||
|
||||
// entity_outline_box_blur.fsh at Radius 2: bilinear taps at -1.5 and 0.5 texels and half
|
||||
// a tap at 2; the colour averaged, the alpha summed.
|
||||
fn blur(uv: vec2<f32>, step: vec2<f32>) -> vec4<f32> {
|
||||
let blurred = tap(uv + step * -1.5) + tap(uv + step * 0.5) + tap(uv + step * 2.0) / 2.0;
|
||||
return vec4(blurred.rgb / 2.5, blurred.a);
|
||||
}
|
||||
|
||||
// BlurDir (1, 0).
|
||||
@fragment fn fs_blur_x(in: PostOut) -> @location(0) vec4<f32> {
|
||||
return blur(in.uv, vec2(1.0 / f32(textureDimensions(source).x), 0.0));
|
||||
}
|
||||
|
||||
// BlurDir (0, 1): up the screen in GL's bottom-up textures, towards row 0 here.
|
||||
@fragment fn fs_blur_y(in: PostOut) -> @location(0) vec4<f32> {
|
||||
return blur(in.uv, vec2(0.0, -1.0 / f32(textureDimensions(source).y)));
|
||||
}
|
||||
|
||||
// blit_screen.fsh, blended by RenderPipelines.ENTITY_OUTLINE_BLIT.
|
||||
@fragment fn fs_blit(in: PostOut) -> @location(0) vec4<f32> {
|
||||
return tap(in.uv);
|
||||
}
|
||||
@@ -405,12 +405,12 @@ pub(super) fn quad_indices(device: &wgpu::Device, quads: u32) -> wgpu::Buffer {
|
||||
}
|
||||
|
||||
/// Whether a run holds only geometry a gbuffers program can draw from the shared atlas: no
|
||||
/// celestial body, cloud, beam, End portal or crack (overlay codes Horizon's entity shader
|
||||
/// draws in its own way; entity.wgsl).
|
||||
/// celestial body, cloud, beam, End portal, crack or energy swirl (overlay codes Horizon's entity
|
||||
/// shader draws in its own way; entity.wgsl).
|
||||
fn plain(run: &[Vertex]) -> bool {
|
||||
run.iter()
|
||||
.step_by(4)
|
||||
.all(|v| !matches!(v.layer, 2 | 4 | 6 | 9 | 10 | 11 | 12))
|
||||
.all(|v| !matches!(v.layer, 2 | 4 | 6 | 9 | 10 | 11 | 12 | 34))
|
||||
}
|
||||
|
||||
/// The programs of entity runs, in the order they draw.
|
||||
|
||||
@@ -204,6 +204,19 @@ impl Gizmos {
|
||||
position[1] + f64::from(size.1),
|
||||
position[2] + half,
|
||||
];
|
||||
self.entity_box(min, max, position, eye_height, living, view);
|
||||
}
|
||||
/// The same for an entity whose box is not centred on its position
|
||||
/// (a painting or an item frame): `min` and `max` are the box.
|
||||
pub fn entity_box(
|
||||
&mut self,
|
||||
min: [f64; 3],
|
||||
max: [f64; 3],
|
||||
position: [f64; 3],
|
||||
eye_height: f32,
|
||||
living: bool,
|
||||
view: (f32, f32),
|
||||
) {
|
||||
self.cuboid(min, max, WHITE, STROKE_WIDTH);
|
||||
if living {
|
||||
let eye = min[1] + f64::from(eye_height);
|
||||
|
||||
@@ -912,6 +912,8 @@ impl HudBatch {
|
||||
// StringDecomposer.iterateFormatted inside Font.drawInternal:
|
||||
// `§` codes inside a span restyle what follows and are not drawn.
|
||||
let text = text.resolved();
|
||||
// Language.getVisualOrder: shaped and reordered for right-to-left text.
|
||||
let text = crate::bidi::visual(&text);
|
||||
let font = &atlas.font;
|
||||
let alpha = (color >> 24) as u8;
|
||||
let mut glyphs = Vec::new();
|
||||
|
||||
@@ -46,6 +46,9 @@ pub struct ItemQuad {
|
||||
pub foil: Foil,
|
||||
/// `MaterialInfo.lightEmission` (block light floor).
|
||||
pub light_emission: u8,
|
||||
/// The quad's sprite has translucent pixels (`MaterialInfo.itemRenderType`
|
||||
/// is a translucent sheet): drawn with the translucent entities.
|
||||
pub translucent: bool,
|
||||
}
|
||||
/// Which glint render type a quad uses.
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
||||
@@ -159,7 +162,7 @@ impl ItemRenderState {
|
||||
let m = pose * layer.matrix;
|
||||
match &layer.geometry {
|
||||
LayerGeometry::Quads(baked) => {
|
||||
submit_quads(baked, layer, self.display, m, sprites, sink);
|
||||
submit_quads(renderer, baked, layer, self.display, m, sprites, sink);
|
||||
}
|
||||
LayerGeometry::Special { model, argument } => special::submit(
|
||||
renderer,
|
||||
@@ -203,6 +206,7 @@ pub fn argb(color: i32) -> u32 {
|
||||
|
||||
/// `ItemFeatureRenderer.prepareMainSubmit` for one cuboid layer.
|
||||
fn submit_quads(
|
||||
renderer: &ItemRenderer,
|
||||
baked: &BakedItemModel,
|
||||
layer: &LayerRender,
|
||||
display: DisplayContext,
|
||||
@@ -216,6 +220,8 @@ fn submit_quads(
|
||||
FoilType::Special => Foil::Special,
|
||||
};
|
||||
let _ = display;
|
||||
// Quads of one layer mostly share a sprite: look its translucency up once.
|
||||
let mut last: Option<(&str, bool)> = None;
|
||||
for q in &baked.quads {
|
||||
let Some(r) = sprites
|
||||
.get(sprite_name(&q.material.sprite))
|
||||
@@ -239,6 +245,16 @@ fn submit_quads(
|
||||
let pos = q.positions.map(|p| m.transform_point3(Vec3::from(p)));
|
||||
let uv =
|
||||
q.uv.map(|[u, v]| [r.u0 + (r.u1 - r.u0) * u, r.v0 + (r.v1 - r.v0) * v]);
|
||||
let translucent = match last {
|
||||
Some((sprite, translucent)) if sprite == q.material.sprite => translucent,
|
||||
_ => {
|
||||
let translucent = renderer
|
||||
.sprites
|
||||
.translucent(sprite_name(&q.material.sprite));
|
||||
last = Some((&q.material.sprite, translucent));
|
||||
translucent
|
||||
}
|
||||
};
|
||||
sink.quad(ItemQuad {
|
||||
pos,
|
||||
uv,
|
||||
@@ -246,6 +262,7 @@ fn submit_quads(
|
||||
color: argb(color),
|
||||
foil,
|
||||
light_emission: q.light_emission,
|
||||
translucent: translucent || q.material.force_translucent,
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -260,6 +277,9 @@ struct FileSprites {
|
||||
root: PathBuf,
|
||||
permutations: Vec<PalettedPermutations>,
|
||||
cache: Mutex<HashMap<String, Option<SpriteMask>>>,
|
||||
/// Sprites with a pixel that is neither fully transparent nor opaque
|
||||
/// (`Transparency.hasTranslucent`), filled in as sprites load.
|
||||
translucent: Mutex<HashMap<String, bool>>,
|
||||
}
|
||||
impl SpriteLookup for FileSprites {
|
||||
fn mask(&self, sprite: &str) -> Option<SpriteMask> {
|
||||
@@ -283,6 +303,13 @@ fn argb_pixels(rgba: &[u8]) -> Vec<u32> {
|
||||
.collect()
|
||||
}
|
||||
impl FileSprites {
|
||||
fn translucent(&self, sprite: &str) -> bool {
|
||||
let known = |this: &Self| this.translucent.lock().ok()?.get(sprite).copied();
|
||||
known(self).unwrap_or_else(|| {
|
||||
self.mask(sprite);
|
||||
known(self).unwrap_or(false)
|
||||
})
|
||||
}
|
||||
fn png(&self, sprite: &str, directory: &str) -> PathBuf {
|
||||
crate::atlas_sources::texture_path(&self.root, sprite, directory)
|
||||
}
|
||||
@@ -303,6 +330,10 @@ impl FileSprites {
|
||||
(w, h, pixels)
|
||||
}
|
||||
};
|
||||
let translucent = argb.iter().any(|p| matches!(p >> 24, 1..=254));
|
||||
if let Ok(mut cache) = self.translucent.lock() {
|
||||
cache.insert(sprite.to_owned(), translucent);
|
||||
}
|
||||
let alpha: Vec<u8> = argb.iter().map(|p| (p >> 24) as u8).collect();
|
||||
let animation = fs::read_to_string(png.with_extension("png.mcmeta"))
|
||||
.ok()
|
||||
@@ -452,6 +483,7 @@ impl ItemRenderer {
|
||||
root: root.to_path_buf(),
|
||||
permutations,
|
||||
cache: Mutex::default(),
|
||||
translucent: Mutex::default(),
|
||||
},
|
||||
baked: Mutex::default(),
|
||||
extents: Mutex::default(),
|
||||
@@ -694,7 +726,12 @@ impl ItemSink for WorldSink<'_> {
|
||||
} else {
|
||||
self.light
|
||||
};
|
||||
let overlay = if q.foil == Foil::None { 0 } else { 7 };
|
||||
let overlay = match (q.foil, q.translucent) {
|
||||
(Foil::None, true) => crate::gfx::TRANSLUCENT_ITEM_OVERLAY,
|
||||
(Foil::None, false) => 0,
|
||||
(_, true) => crate::gfx::TRANSLUCENT_GLINT_ITEM_OVERLAY,
|
||||
(_, false) => 7,
|
||||
};
|
||||
for (p, uv) in q.pos.iter().zip(q.uv) {
|
||||
self.out.push(Vertex {
|
||||
normal: [0; 4],
|
||||
|
||||
@@ -358,6 +358,7 @@ mod tests {
|
||||
color,
|
||||
foil: Foil::None,
|
||||
light_emission: 0,
|
||||
translucent: false,
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -119,6 +119,7 @@ fn draw(
|
||||
color,
|
||||
foil,
|
||||
light_emission: 0,
|
||||
translucent: false,
|
||||
});
|
||||
}
|
||||
for (child_name, child) in &part.children {
|
||||
|
||||
@@ -2,7 +2,12 @@
|
||||
//! player state (inventory, health, game mode) that the host's gameplay code
|
||||
//! operates on when it acts for them.
|
||||
use super::{Joined, PlayerSender, entity::Tracker};
|
||||
use crate::{items::ItemStack, motion::PlayerMotion};
|
||||
use crate::{
|
||||
items::ItemStack,
|
||||
motion::PlayerMotion,
|
||||
pose::{Dimensions, Pose},
|
||||
};
|
||||
use horizon_core::geometry::{Aabb, Vec3};
|
||||
use horizon_protocol::{
|
||||
item::Stack,
|
||||
packets::{configuration::ClientInformation, play},
|
||||
@@ -407,8 +412,41 @@ impl Guest {
|
||||
}
|
||||
f
|
||||
}
|
||||
/// `Player.getDesiredPose`: sleeping, gliding, then the body's own pose
|
||||
/// (swimming, crawling, crouching) once its server tick has run.
|
||||
pub fn pose(&self) -> Pose {
|
||||
if self.sleeping {
|
||||
Pose::Sleeping
|
||||
} else if self.fall_flying {
|
||||
Pose::FallFlying
|
||||
} else if let Some(m) = &self.motion {
|
||||
m.pose
|
||||
} else if self.sneaking && !self.flying {
|
||||
Pose::Crouching
|
||||
} else {
|
||||
Pose::Standing
|
||||
}
|
||||
}
|
||||
/// `Avatar.POSES` for the guest's pose, with its scale attribute.
|
||||
pub fn dimensions(&self) -> Dimensions {
|
||||
let pose = self.pose();
|
||||
match &self.motion {
|
||||
Some(m) if m.pose == pose => m.dimensions,
|
||||
_ => pose.dimensions(1.0),
|
||||
}
|
||||
}
|
||||
/// `Entity.getBoundingBox`.
|
||||
pub fn bounds(&self) -> Aabb {
|
||||
let [x, y, z] = self.position;
|
||||
let d = self.dimensions();
|
||||
let half = f64::from(d.width) / 2.0;
|
||||
Aabb::new(
|
||||
Vec3::new(x - half, y, z - half),
|
||||
Vec3::new(x + half, y + f64::from(d.height), z + half),
|
||||
)
|
||||
}
|
||||
pub fn eye_height(&self) -> f64 {
|
||||
if self.sneaking { 1.27 } else { 1.62 }
|
||||
f64::from(self.dimensions().eye_height)
|
||||
}
|
||||
/// `Player.isWithinBlockInteractionRange(pos, buffer)` for the guest:
|
||||
/// its block interaction range is 4.5, 5 in creative.
|
||||
|
||||
@@ -2329,18 +2329,8 @@ impl Hub {
|
||||
}
|
||||
// getBoundingBox: the box of the body's pose (swimming and crawling
|
||||
// are 0.6 tall), so a swimmer under a ceiling isn't sent back.
|
||||
let (half, height) = if g.fall_flying {
|
||||
(0.3, 0.6)
|
||||
} else if let Some(m) = &g.motion {
|
||||
(
|
||||
f64::from(m.dimensions.width) / 2.0,
|
||||
f64::from(m.dimensions.height),
|
||||
)
|
||||
} else if g.sneaking {
|
||||
(0.3, 1.5)
|
||||
} else {
|
||||
(0.3, 1.8)
|
||||
};
|
||||
let d = g.dimensions();
|
||||
let (half, height) = (f64::from(d.width) / 2.0, f64::from(d.height));
|
||||
let at = |p: [f64; 3]| {
|
||||
Aabb::new(
|
||||
Vec3::new(p[0] - half, p[1], p[2] - half),
|
||||
|
||||
@@ -15,6 +15,7 @@ pub mod auto_jump;
|
||||
pub mod banner;
|
||||
pub mod beehive;
|
||||
pub mod benchmark;
|
||||
pub mod bidi;
|
||||
pub mod biome;
|
||||
pub mod block_entities;
|
||||
pub mod block_entity_anim;
|
||||
@@ -74,6 +75,7 @@ pub mod fog;
|
||||
pub mod font;
|
||||
pub mod friends_screen;
|
||||
pub mod game_mode_switcher;
|
||||
pub mod game_rules_screen;
|
||||
pub mod gamepad;
|
||||
pub mod gfx;
|
||||
pub mod gizmos;
|
||||
|
||||
@@ -11,6 +11,7 @@ pub mod ambient;
|
||||
pub mod anim;
|
||||
pub mod animal_brain;
|
||||
pub mod aquatic;
|
||||
pub mod armadillo;
|
||||
pub mod axolotl;
|
||||
pub mod bat;
|
||||
pub mod bee;
|
||||
@@ -31,8 +32,11 @@ pub mod enderman;
|
||||
pub mod equine;
|
||||
pub mod farm;
|
||||
pub mod finalize;
|
||||
pub mod fox;
|
||||
pub mod frog;
|
||||
pub mod gear;
|
||||
pub mod goals;
|
||||
pub mod goat;
|
||||
pub mod golem;
|
||||
pub mod guardian;
|
||||
pub mod happy_ghast;
|
||||
@@ -47,6 +51,7 @@ pub mod monster_brain;
|
||||
pub mod nautilus;
|
||||
pub mod nav;
|
||||
pub mod nether;
|
||||
pub mod panda;
|
||||
pub mod path;
|
||||
pub mod phantom;
|
||||
pub mod piglin;
|
||||
@@ -66,6 +71,7 @@ pub mod spawning;
|
||||
pub mod species;
|
||||
pub mod strider;
|
||||
pub mod sulfur_cube;
|
||||
pub mod sync;
|
||||
pub mod tame;
|
||||
pub mod trader;
|
||||
pub mod transport_items;
|
||||
@@ -527,6 +533,8 @@ pub enum DamageKind {
|
||||
InWall,
|
||||
Thorns,
|
||||
SonicBoom,
|
||||
/// `freeze`: a fully frozen mob in powder snow.
|
||||
Freeze,
|
||||
/// `fell_out_of_world`: below the world's floor.
|
||||
OutOfWorld,
|
||||
}
|
||||
@@ -596,6 +604,7 @@ impl Damage {
|
||||
| DamageKind::InWall
|
||||
| DamageKind::Generic
|
||||
| DamageKind::SonicBoom
|
||||
| DamageKind::Freeze
|
||||
| DamageKind::OutOfWorld
|
||||
)
|
||||
}
|
||||
@@ -620,6 +629,7 @@ impl Damage {
|
||||
| DamageKind::Cactus
|
||||
| DamageKind::Generic
|
||||
| DamageKind::InWall
|
||||
| DamageKind::Freeze
|
||||
| DamageKind::OutOfWorld
|
||||
)
|
||||
}
|
||||
@@ -646,6 +656,9 @@ pub struct Attrs {
|
||||
pub struct Extra {
|
||||
/// Players: crouching and riding (HumanoidModel's poses).
|
||||
pub crouching: bool,
|
||||
/// A player's or mannequin's synced pose (`Entity.DATA_POSE`), which
|
||||
/// sets its box (`Avatar.POSES`).
|
||||
pub avatar_pose: crate::pose::Pose,
|
||||
/// Players: sprinting (`Entity.DATA_SHARED_FLAGS_ID`), for the dust they raise.
|
||||
pub sprinting: bool,
|
||||
pub passenger: bool,
|
||||
@@ -788,12 +801,16 @@ pub struct Extra {
|
||||
/// A goat's `IsScreamingGoat` and the horns it lost (left, right).
|
||||
pub screaming: bool,
|
||||
pub lost_horns: [bool; 2],
|
||||
/// A goat's lowered head and ram behaviours.
|
||||
pub goat: goat::State,
|
||||
/// Attribute bases and permanent modifiers set by `finalizeSpawn`.
|
||||
pub attributes: Option<Box<finalize::Attributes>>,
|
||||
/// Brain memories `finalizeSpawn` sets: (memory, value, ticks to expiry).
|
||||
pub memories: Vec<(&'static str, i64, i64)>,
|
||||
/// A camel's `LAST_POSE_CHANGE_TICK`.
|
||||
pub pose_change_tick: i64,
|
||||
/// `Entity.DATA_POSE` as a mob of a server has it (`sync::pose_id`).
|
||||
pub pose: u8,
|
||||
/// The school leader a fish follows, and a tropical fish that swims alone.
|
||||
pub leader: Option<u32>,
|
||||
pub lone: bool,
|
||||
@@ -801,6 +818,12 @@ pub struct Extra {
|
||||
pub brain: Option<Box<monster_brain::Brain>>,
|
||||
/// A bee's nectar, anger, flower and hive state.
|
||||
pub bee: Option<Box<bee::Bee>>,
|
||||
/// An armadillo's state and scare brain.
|
||||
pub armadillo: armadillo::State,
|
||||
/// A frog's brain memories and tongue target.
|
||||
pub frog: frog::State,
|
||||
/// A panda's flags, counters and amounts.
|
||||
pub panda: Option<Box<panda::State>>,
|
||||
}
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct Mob {
|
||||
@@ -903,6 +926,18 @@ pub enum MobEvent {
|
||||
mob: u32,
|
||||
at: [f64; 3],
|
||||
},
|
||||
/// `LivingEntity.baseTick`'s drowning damage: entity event 67, the
|
||||
/// clients' `makeDrownParticles`.
|
||||
Drown {
|
||||
mob: u32,
|
||||
at: [f64; 3],
|
||||
},
|
||||
/// `Level.broadcastEntityEvent` for a mob: guests run `sync::entity_event`.
|
||||
EntityEvent {
|
||||
mob: u32,
|
||||
event: i8,
|
||||
at: [f64; 3],
|
||||
},
|
||||
Sound(&'static str, [f64; 3], f32, f32),
|
||||
/// `ChestBlockEntity.startOpen` / `stopOpen` by a copper golem: the
|
||||
/// chest's opener count goes up or down (interaction range 3).
|
||||
@@ -1010,6 +1045,13 @@ pub enum MobEvent {
|
||||
amount: f32,
|
||||
source: Damage,
|
||||
},
|
||||
/// `LivingEntity.knockback(power, x, z)`: pushed away from the direction.
|
||||
Knockback {
|
||||
target: Who,
|
||||
power: f64,
|
||||
x: f64,
|
||||
z: f64,
|
||||
},
|
||||
/// Set another mob's attack target (HurtByTargetGoal alerts).
|
||||
SetTarget {
|
||||
mob: u32,
|
||||
@@ -1244,6 +1286,10 @@ pub enum MobEvent {
|
||||
center: [f64; 3],
|
||||
spread: [f64; 3],
|
||||
},
|
||||
/// `Panda.afterSneeze`: the grown pandas within 10 blocks start.
|
||||
PandaSneezed {
|
||||
mob: u32,
|
||||
},
|
||||
}
|
||||
/// How a spawned-by-kind mob should be set up.
|
||||
#[derive(Clone, Debug, Default)]
|
||||
@@ -1281,6 +1327,8 @@ pub struct Seen {
|
||||
pub target: Option<Who>,
|
||||
pub health: f32,
|
||||
pub variant: u8,
|
||||
/// A slime's or magma cube's size.
|
||||
pub size: i32,
|
||||
/// Dye colour (sheep).
|
||||
pub color: u8,
|
||||
/// A villager's claimed job site and bed.
|
||||
@@ -1348,6 +1396,10 @@ impl Mob {
|
||||
.max((f64::from(ticks) * self.burning_time()).ceil() as i32);
|
||||
self.ticks_frozen = 0;
|
||||
}
|
||||
/// `Entity.isFullyFrozen`.
|
||||
pub fn is_fully_frozen(&self) -> bool {
|
||||
self.ticks_frozen >= crate::motion::PlayerMotion::TICKS_REQUIRED_TO_FREEZE
|
||||
}
|
||||
|
||||
/// What other mobs see of this one this tick (`player`: the player's
|
||||
/// UUID, which villagers' reputation is keyed by).
|
||||
@@ -1376,6 +1428,7 @@ impl Mob {
|
||||
target: m.ai.target,
|
||||
health: m.health,
|
||||
variant: m.extra.variant,
|
||||
size: m.extra.size,
|
||||
color: m.extra.color,
|
||||
job: villager.then_some(m.extra.home).flatten(),
|
||||
bed: villager.then_some(m.extra.bed).flatten(),
|
||||
@@ -1554,6 +1607,9 @@ pub struct PlayerView {
|
||||
pub last_hurt: Option<(Who, i64)>,
|
||||
pub velocity: Vec3,
|
||||
pub yaw: f32,
|
||||
/// `Player.isSprinting` and `isPassenger` (what scares an armadillo).
|
||||
pub sprinting: bool,
|
||||
pub riding: bool,
|
||||
/// The Hero of the Village effect.
|
||||
pub hero: bool,
|
||||
/// The active effects, without namespace (`slowness`, `poison`...).
|
||||
@@ -1578,6 +1634,8 @@ pub struct MobWorld<'a> {
|
||||
pub world: &'a World,
|
||||
pub registry: &'a horizon_world::registry::BlockRegistry,
|
||||
pub facts: &'a BlockFacts,
|
||||
/// Fluid states and flow facts, for the currents that push mobs.
|
||||
pub fluid_tables: &'a crate::motion::FluidResources,
|
||||
pub movement: &'a horizon_world::movement_properties::BoundMovementProperties,
|
||||
pub collision: &'a BlockCollisionRegistry<'a>,
|
||||
pub data: &'a MobData,
|
||||
@@ -1820,12 +1878,24 @@ impl Mob {
|
||||
return false;
|
||||
}
|
||||
let mut amount = if reflected { 1000.0 } else { amount };
|
||||
if self.species() == Species::Armadillo {
|
||||
amount = armadillo::reduce_damage(self, amount);
|
||||
}
|
||||
if source.kind == DamageKind::FallingBlock
|
||||
&& !species::body_slot(self.species())
|
||||
&& self.extra.head.is_some()
|
||||
{
|
||||
amount *= 0.75;
|
||||
}
|
||||
// LivingEntity.hurtServer: `#freeze_hurts_extra_types` take five times.
|
||||
if source.kind == DamageKind::Freeze
|
||||
&& matches!(
|
||||
self.species(),
|
||||
Species::Strider | Species::Blaze | Species::MagmaCube
|
||||
)
|
||||
{
|
||||
amount *= 5.0;
|
||||
}
|
||||
let (worn, worn_toughness) = self.worn_armor();
|
||||
if !source.bypasses_armor() && self.kind.armor + worn > 0.0 {
|
||||
// CombatRules.getDamageAfterAbsorb
|
||||
@@ -1968,6 +2038,11 @@ impl Mob {
|
||||
pub fn has_effect(&self, effect: &str) -> bool {
|
||||
self.extra.effects.iter().any(|e| e.0 == effect)
|
||||
}
|
||||
/// `LivingEntity.isCurrentlyGlowing`: the Glowing effect or the `Glowing` tag (on a
|
||||
/// server's client, `glowing` holds shared flag 6 instead).
|
||||
pub fn is_currently_glowing(&self) -> bool {
|
||||
self.glowing || self.has_effect("minecraft:glowing")
|
||||
}
|
||||
pub fn add_effect(&mut self, effect: &'static str, ticks: i32, amplifier: i32) {
|
||||
if let Some(e) = self.extra.effects.iter_mut().find(|e| e.0 == effect) {
|
||||
// MobEffectInstance.update: a stronger effect replaces the old one
|
||||
@@ -1999,8 +2074,18 @@ impl Mob {
|
||||
}
|
||||
/// LivingEntity.getVoicePitch.
|
||||
pub fn voice_pitch(&mut self) -> f32 {
|
||||
// HappyGhast.getVoicePitch: always 1.0, babies and all.
|
||||
if self.species() == Species::HappyGhast {
|
||||
return 1.0;
|
||||
}
|
||||
// ZombieVillager.getVoicePitch: babies are 2.0, not 1.5.
|
||||
let baby = if self.species() == Species::ZombieVillager {
|
||||
2.0
|
||||
} else {
|
||||
1.5
|
||||
};
|
||||
let pitch = (self.random.next_float() - self.random.next_float()) * 0.2
|
||||
+ if self.is_baby() { 1.5 } else { 1.0 };
|
||||
+ if self.is_baby() { baby } else { 1.0 };
|
||||
// Bat.getVoicePitch: a little lower.
|
||||
if self.species() == Species::Bat {
|
||||
pitch * 0.95
|
||||
|
||||
@@ -106,6 +106,7 @@ pub enum Family {
|
||||
Strider,
|
||||
Bat,
|
||||
Bee,
|
||||
Panda,
|
||||
PolarBear,
|
||||
}
|
||||
pub fn family(s: Species) -> Family {
|
||||
@@ -168,6 +169,7 @@ pub fn family(s: Species) -> Family {
|
||||
S::Strider => Family::Strider,
|
||||
S::Bat => Family::Bat,
|
||||
S::Bee => Family::Bee,
|
||||
S::Panda => Family::Panda,
|
||||
S::PolarBear => Family::PolarBear,
|
||||
_ => Family::Generic,
|
||||
}
|
||||
@@ -180,6 +182,9 @@ pub fn register(m: &mut Mob) {
|
||||
Family::Undead => undead::register(m),
|
||||
Family::Hostile => hostile::register(m),
|
||||
Family::Aquatic => aquatic::register(m),
|
||||
Family::Generic if m.species() == Species::Armadillo => super::armadillo::register(m),
|
||||
Family::Generic if m.species() == Species::Frog => super::frog::register(m),
|
||||
Family::Generic if m.species() == Species::Fox => super::fox::register(m),
|
||||
Family::Generic => {
|
||||
generic(m);
|
||||
if m.species() == Species::Rabbit {
|
||||
@@ -188,6 +193,9 @@ pub fn register(m: &mut Mob) {
|
||||
if m.species() == Species::Turtle {
|
||||
super::turtle::register(m);
|
||||
}
|
||||
if m.species() == Species::Goat {
|
||||
super::goat::register(m);
|
||||
}
|
||||
}
|
||||
Family::Inert => {}
|
||||
Family::Witch => super::witch::register(m),
|
||||
@@ -229,6 +237,7 @@ pub fn register(m: &mut Mob) {
|
||||
Family::Strider => super::strider::register(m),
|
||||
Family::Bat => super::bat::register(m),
|
||||
Family::Bee => super::bee::register(m),
|
||||
Family::Panda => super::panda::register(m),
|
||||
Family::PolarBear => super::polar_bear::register(m),
|
||||
}
|
||||
// FloatGoal's constructor lets the navigation float, as do the
|
||||
@@ -411,7 +420,10 @@ pub fn tick(
|
||||
}
|
||||
// Mob.tickHeadTurn -> BodyRotationControl.clientTick (a frozen
|
||||
// creaking's waits).
|
||||
if m.alive() && (!super::creaking::is_creaking(m) || super::creaking::can_move(m)) {
|
||||
if m.alive()
|
||||
&& (!super::creaking::is_creaking(m) || super::creaking::can_move(m))
|
||||
&& !super::armadillo::scared(m)
|
||||
{
|
||||
m.body_rotation_tick();
|
||||
}
|
||||
events.append(&mut m.outbox);
|
||||
@@ -433,6 +445,9 @@ pub fn tick(
|
||||
if m.species() == Species::Bee {
|
||||
super::bee::tick(m, level_random);
|
||||
}
|
||||
if m.species() == Species::Panda {
|
||||
super::panda::tick(m, w, events);
|
||||
}
|
||||
true
|
||||
}
|
||||
/// LivingEntity.updateSwingTime (6-tick swing duration).
|
||||
@@ -448,6 +463,16 @@ pub fn tick_swing(m: &mut Mob) {
|
||||
}
|
||||
m.anim.attack_anim = m.ai.swing_time as f32 / 6.0;
|
||||
}
|
||||
/// `Entity.isInRain`: rain at the block position, or at the top of the box.
|
||||
pub(super) fn in_rain(m: &Mob, w: &MobWorld) -> bool {
|
||||
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;
|
||||
w.raining_at(x, y, z) || w.raining_at(x, top, z)
|
||||
}
|
||||
/// `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
|
||||
@@ -458,15 +483,7 @@ fn axolotl_air_supply(m: &mut Mob, w: &MobWorld) {
|
||||
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 {
|
||||
if m.fluids.in_water || in_rain(m, w) {
|
||||
m.air = super::axolotl::MAX_AIR;
|
||||
} else {
|
||||
m.air = before - 1;
|
||||
@@ -494,8 +511,9 @@ fn base_tick(m: &mut Mob, w: &MobWorld, events: &mut Vec<MobEvent>) {
|
||||
);
|
||||
m.damage(fire, Damage::of(DamageKind::InFire));
|
||||
}
|
||||
// Fire: water extinguishes, lava ignites, burning hurts every second.
|
||||
if m.fluids.in_water && m.fire_ticks > 0 {
|
||||
// Fire: water or rain (Entity.isInRain) extinguishes, lava ignites,
|
||||
// burning hurts every second.
|
||||
if m.fire_ticks > 0 && (m.fluids.in_water || in_rain(m, w)) {
|
||||
m.fire_ticks = 0;
|
||||
events.push(MobEvent::Sound(
|
||||
"entity.generic.extinguish_fire",
|
||||
@@ -524,14 +542,19 @@ fn base_tick(m: &mut Mob, w: &MobWorld, events: &mut Vec<MobEvent>) {
|
||||
}
|
||||
m.first_tick = false;
|
||||
if m.alive() {
|
||||
// LivingEntity.baseTick: suffocation inside full blocks.
|
||||
// LivingEntity.baseTick: suffocation inside full blocks. Entity.isInWall
|
||||
// is false under noPhysics, which Vex.tick holds while it ticks.
|
||||
let (ex, ey, ez) = (
|
||||
m.position.x.floor() as i32,
|
||||
m.eye_y().floor() as i32,
|
||||
m.position.z.floor() as i32,
|
||||
);
|
||||
let at_eye = w.facts.get(w.world.block(ex, ey, ez));
|
||||
if at_eye.full_collision && at_eye.solid_render && m.width() < 1.0 {
|
||||
if at_eye.full_collision
|
||||
&& at_eye.solid_render
|
||||
&& m.width() < 1.0
|
||||
&& m.species() != Species::Vex
|
||||
{
|
||||
m.damage(1.0, Damage::of(DamageKind::InWall));
|
||||
}
|
||||
// Air supply and drowning (WaterAnimal.handleAirSupply for aquatic mobs).
|
||||
@@ -551,17 +574,20 @@ fn base_tick(m: &mut Mob, w: &MobWorld, events: &mut Vec<MobEvent>) {
|
||||
m.air -= 1;
|
||||
if m.air == -20 {
|
||||
m.air = 0;
|
||||
events.push(MobEvent::Drown {
|
||||
mob: m.id,
|
||||
at: [m.position.x, m.position.y, m.position.z],
|
||||
});
|
||||
m.damage(2.0, Damage::of(DamageKind::Drown));
|
||||
}
|
||||
} else if m.air < 300 {
|
||||
m.air = (m.air + 4).min(300);
|
||||
}
|
||||
|
||||
if m.kind.flags.water_sensitive
|
||||
&& (m.fluids.in_water || w.raining && w.can_see_sky(ex, ey, ez))
|
||||
{
|
||||
if m.kind.flags.water_sensitive && (m.fluids.in_water || in_rain(m, w)) {
|
||||
m.damage(1.0, Damage::of(DamageKind::Drown));
|
||||
}
|
||||
freeze_tick(m, w);
|
||||
}
|
||||
tick_effects(m);
|
||||
if m.species() == Species::Illusioner {
|
||||
@@ -591,6 +617,57 @@ fn base_tick(m: &mut Mob, w: &MobWorld, events: &mut Vec<MobEvent>) {
|
||||
// Entity.baseTick ends with Leashable.tickLeash.
|
||||
super::leash::tick(m, w, events);
|
||||
}
|
||||
/// `LivingEntity.aiStep`'s "freezing" with `InsideBlockEffectType.FREEZE`:
|
||||
/// powder snow builds `ticks_frozen` up, anywhere else it melts twice as
|
||||
/// fast, and a fully frozen mob takes one freeze damage every 40 ticks.
|
||||
fn freeze_tick(m: &mut Mob, w: &MobWorld) {
|
||||
let in_snow = in_powder_snow(m, w);
|
||||
if !in_snow && m.ticks_frozen == 0 {
|
||||
return;
|
||||
}
|
||||
let can_freeze = can_freeze(m, w);
|
||||
if in_snow && can_freeze {
|
||||
m.ticks_frozen =
|
||||
(m.ticks_frozen + 1).min(crate::motion::PlayerMotion::TICKS_REQUIRED_TO_FREEZE);
|
||||
} else {
|
||||
m.ticks_frozen = (m.ticks_frozen - 2).max(0);
|
||||
}
|
||||
if m.age.is_multiple_of(40) && m.is_fully_frozen() && can_freeze {
|
||||
m.damage(1.0, Damage::of(DamageKind::Freeze));
|
||||
}
|
||||
}
|
||||
/// Whether the mob's box (shrunk by `1.0E-5`, as in `Entity.checkInsideBlocks`)
|
||||
/// touches powder snow.
|
||||
fn in_powder_snow(m: &Mob, w: &MobWorld) -> bool {
|
||||
let b = m.bounds().deflate(Vec3::new(1e-5, 1e-5, 1e-5));
|
||||
let (min, max) = (b.min_position(), b.max_position());
|
||||
for x in min.x.floor() as i32..=max.x.floor() as i32 {
|
||||
for y in min.y.floor() as i32..=max.y.floor() as i32 {
|
||||
for z in min.z.floor() as i32..=max.z.floor() as i32 {
|
||||
if &*w.facts.get(w.world.block(x, y, z)).name == "minecraft:powder_snow" {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
false
|
||||
}
|
||||
/// `LivingEntity.canFreeze`: not a `#freeze_immune_entity_types` mob and no
|
||||
/// worn piece in `#freeze_immune_wearables`.
|
||||
fn can_freeze(m: &Mob, w: &MobWorld) -> bool {
|
||||
if w.data
|
||||
.entity_has("minecraft:freeze_immune_entity_types", &m.kind.id)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
let immune = w
|
||||
.items
|
||||
.and_then(|i| i.item_tags.get("minecraft:freeze_immune_wearables"));
|
||||
![&m.extra.head, &m.extra.chest, &m.extra.legs, &m.extra.feet]
|
||||
.into_iter()
|
||||
.flatten()
|
||||
.any(|item| immune.is_some_and(|t| t.contains(&**item)))
|
||||
}
|
||||
/// `Mob.playAmbientSound` on demand: a spawn egg or a mob bucket just placed `m`.
|
||||
pub fn ambient_sound(m: &mut Mob, w: &MobWorld, events: &mut Vec<MobEvent>) {
|
||||
play_ambient(m, w, events);
|
||||
@@ -676,6 +753,9 @@ fn ai_step(m: &mut Mob, w: &MobWorld, level_random: &mut LegacyRandom, events: &
|
||||
.is_some_and(|t| t.position.distance_squared(m.position) > 121.0);
|
||||
super::witch::ai_step(m, events, far);
|
||||
}
|
||||
if m.species() == Species::Fox {
|
||||
super::fox::pre_ai_step(m);
|
||||
}
|
||||
// AbstractHorse.aiStep: the tail swishes now and then (moveTail).
|
||||
if super::equine::is_equine(m.species()) {
|
||||
super::equine::tail_tick(m);
|
||||
@@ -895,7 +975,7 @@ fn sun_burn_tick(m: &mut Mob, w: &MobWorld) -> bool {
|
||||
m.position.z.floor() as i32,
|
||||
);
|
||||
let br = w.brightness(x, y, z);
|
||||
let wet = m.fluids.in_water || w.raining && w.can_see_sky(x, y, z);
|
||||
let wet = m.fluids.in_water || in_rain(m, w);
|
||||
br > 0.5 && m.random.next_float() * 30.0 < (br - 0.4) * 2.0 && !wet && w.can_see_sky(x, y, z)
|
||||
}
|
||||
/// Mob.serverAiStep
|
||||
@@ -1001,6 +1081,8 @@ fn custom_server_ai_step(m: &mut Mob, cx: &mut Cx) {
|
||||
Species::Evoker if m.extra.counter > 0 => m.extra.counter -= 1,
|
||||
Species::Bat => super::bat::server_ai_step(m, cx.w, cx.events),
|
||||
Species::Bee => super::bee::server_ai_step(m, cx),
|
||||
Species::Armadillo => super::armadillo::server_ai_step(m, cx),
|
||||
Species::Frog => super::frog::server_ai_step(m, cx.w),
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
@@ -1054,6 +1136,10 @@ fn species_ai_step(
|
||||
}
|
||||
Species::Nautilus | Species::ZombieNautilus => super::nautilus::ai_step(m, w, events),
|
||||
Species::Chicken => farm::chicken_ai_step(m, w, events),
|
||||
Species::Armadillo => super::armadillo::tick(m),
|
||||
Species::Frog => super::frog::tick(m),
|
||||
Species::Goat => super::goat::tick(m),
|
||||
Species::Fox => super::fox::ai_step(m, w, events),
|
||||
Species::Turtle => super::turtle::ai_step(m, w, events),
|
||||
Species::Sheep if m.extra.anim_ticks > 0 => {
|
||||
m.extra.anim_ticks -= 1;
|
||||
@@ -1164,6 +1250,18 @@ pub fn voice_sounds(m: &Mob) -> [&'static str; 3] {
|
||||
match m.species() {
|
||||
Species::Nautilus | Species::ZombieNautilus => super::nautilus::sounds(m),
|
||||
Species::SulfurCube => super::sulfur_cube::sounds(m),
|
||||
Species::Armadillo => super::armadillo::sounds(m),
|
||||
// Panda.getAmbientSound.
|
||||
Species::Panda => {
|
||||
let ambient = if super::panda::is_aggressive(m) {
|
||||
"entity.panda.aggressive_ambient"
|
||||
} else if super::panda::is_worried(m) {
|
||||
"entity.panda.worried_ambient"
|
||||
} else {
|
||||
m.kind.sounds[0]
|
||||
};
|
||||
[ambient, m.kind.sounds[1], m.kind.sounds[2]]
|
||||
}
|
||||
// The sound variant's baby_sounds (classic sets, like the rest here).
|
||||
Species::Pig if m.is_baby() => [
|
||||
"entity.baby_pig.ambient",
|
||||
@@ -1186,6 +1284,10 @@ pub fn feed_animal(
|
||||
m: &mut Mob,
|
||||
level: &mut impl RandomSource,
|
||||
) -> Option<Option<(&'static str, f32, f32)>> {
|
||||
// Armadillo.mobInteract: a ball takes no food.
|
||||
if super::armadillo::scared(m) {
|
||||
return None;
|
||||
}
|
||||
if m.extra.age == 0 && m.extra.in_love <= 0 {
|
||||
m.extra.in_love = 600;
|
||||
m.extra.love_player = true;
|
||||
@@ -1362,6 +1464,11 @@ pub fn accepts_damage(m: &mut Mob, source: &Damage, _amount: f32) -> bool {
|
||||
super::bee::hurt(m);
|
||||
true
|
||||
}
|
||||
// Panda.hurtServer: up first, whatever the damage.
|
||||
Species::Panda => {
|
||||
super::panda::hurt(m);
|
||||
true
|
||||
}
|
||||
Species::Enderman => super::enderman::on_damage(m, source),
|
||||
Species::WitherSkeleton | Species::Blaze | Species::MagmaCube | Species::Strider
|
||||
if source.is_fire() =>
|
||||
@@ -1376,6 +1483,7 @@ pub fn after_hurt(m: &mut Mob, source: &Damage) {
|
||||
m.ai.no_action_time = 0;
|
||||
match m.species() {
|
||||
Species::Enderman => super::enderman::after_hurt(m, source),
|
||||
Species::Armadillo => super::armadillo::after_hurt(m, source),
|
||||
Species::Shulker => super::shulker::after_hurt(m),
|
||||
Species::Warden => super::warden::hurt(m, source),
|
||||
Species::Wolf => super::tame::after_hurt(m, source),
|
||||
@@ -1413,6 +1521,9 @@ pub fn do_hurt_target(m: &mut Mob, who: Who, cx: &mut Cx) {
|
||||
if m.species() == Species::Ravager {
|
||||
super::ravager::on_attack(m, cx.events);
|
||||
}
|
||||
if m.species() == Species::Panda {
|
||||
super::panda::bit(m);
|
||||
}
|
||||
if super::creaking::is_creaking(m) {
|
||||
super::creaking::on_attack(m, cx.events);
|
||||
}
|
||||
@@ -1491,6 +1602,12 @@ pub fn do_hurt_target(m: &mut Mob, who: Who, cx: &mut Cx) {
|
||||
if super::creaking::is_creaking(m) {
|
||||
m.voice("entity.creaking.attack", cx.events);
|
||||
}
|
||||
// Panda.playAttackSound: playSound(PANDA_BITE, 1.0, 1.0).
|
||||
if m.species() == Species::Panda && !m.silent {
|
||||
let at = [m.position.x, m.position.y, m.position.z];
|
||||
cx.events
|
||||
.push(MobEvent::Sound("entity.panda.bite", at, 1.0, 1.0));
|
||||
}
|
||||
// Axolotl.playAttackSound: playSound(AXOLOTL_ATTACK, 1.0, 1.0).
|
||||
if m.species() == Species::Axolotl {
|
||||
let at = [m.position.x, m.position.y, m.position.z];
|
||||
@@ -1682,8 +1799,20 @@ pub fn step_sound(
|
||||
S::Husk => quiet("entity.husk.step"),
|
||||
S::Drowned => quiet("entity.drowned.step"),
|
||||
S::ZombieVillager => quiet("entity.zombie_villager.step"),
|
||||
S::CamelHusk => Some(("entity.camel_husk.step", 0.4, 1.0)),
|
||||
S::Camel => Some(("entity.camel.step", 1.0, 1.0)),
|
||||
// Camel.playStepSound and CamelHusk.playStepSound: sand and concrete
|
||||
// powder have their own step.
|
||||
S::CamelHusk | S::Camel => {
|
||||
let sand = w.has_tag(
|
||||
"minecraft:camel_sand_step_sound_blocks",
|
||||
&w.facts.get(state).name,
|
||||
);
|
||||
Some(match (m.species(), sand) {
|
||||
(S::Camel, true) => ("entity.camel.step_sand", 1.0, 1.0),
|
||||
(S::Camel, false) => ("entity.camel.step", 1.0, 1.0),
|
||||
(_, true) => ("entity.camel_husk.step_sand", 0.4, 1.0),
|
||||
(_, false) => ("entity.camel_husk.step", 0.4, 1.0),
|
||||
})
|
||||
}
|
||||
S::IronGolem => Some(("entity.iron_golem.step", 1.0, 1.0)),
|
||||
// CopperGolem.playStepSound: its oxidation level's step.
|
||||
S::CopperGolem => Some((super::copper_golem::sound(m, "step"), 1.0, 1.0)),
|
||||
|
||||
@@ -133,6 +133,11 @@ pub fn ai_step(m: &mut Mob) {
|
||||
if m.extra.egg_time > 0 {
|
||||
m.extra.egg_time -= 1;
|
||||
}
|
||||
animate(m);
|
||||
}
|
||||
/// `Allay.tick`'s client half: the hold and spin clocks (the server's and a
|
||||
/// client's).
|
||||
pub fn animate(m: &mut Mob) {
|
||||
let holding = m.extra.main_hand.is_some();
|
||||
let v = &mut m.anim.v;
|
||||
v[HOLD] = (v[HOLD] + if holding { 1.0 } else { -1.0 }).clamp(0.0, 5.0);
|
||||
|
||||
@@ -259,7 +259,7 @@ pub fn ai_step(m: &mut Mob, w: &MobWorld, events: &mut Vec<MobEvent>) {
|
||||
}
|
||||
S::Dolphin => {
|
||||
// Dolphin.tick: moisture out of water, hop on land.
|
||||
if m.fluids.in_water || w.raining {
|
||||
if m.fluids.in_water || super::ai::in_rain(m, w) {
|
||||
m.extra.counter = 2400;
|
||||
} else {
|
||||
m.extra.counter -= 1;
|
||||
|
||||
@@ -0,0 +1,407 @@
|
||||
//! Armadillos (`Armadillo`, `ArmadilloAi`): a player who sprints or rides
|
||||
//! nearby, an undead mob or the last attacker scares one into a ball (IDLE,
|
||||
//! ROLLING, SCARED, UNROLLING); it peeks out now and then, and a ball takes
|
||||
//! less damage. The brain's `ARMADILLO_SCARE_DETECTED` sensor, its
|
||||
//! `DANGER_DETECTED_RECENTLY` memory and the `ArmadilloBallUp` behaviour are
|
||||
//! kept in `State`; the walking behaviours are the goals of `register`.
|
||||
//! AnimState states: roll out, roll up, peek.
|
||||
use super::{
|
||||
Damage, DamageKind, Mob, MobEvent, MobWorld, Species, Who,
|
||||
goals::{
|
||||
Cx, Float, Goal, LOOK, LookAtPlayer, MOVE, Panic, RandomLookAround, Stroll, goal_clone,
|
||||
},
|
||||
};
|
||||
use horizon_core::{geometry::Vec3, random::RandomSource};
|
||||
|
||||
pub const IDLE: u8 = 0;
|
||||
pub const ROLLING: u8 = 1;
|
||||
pub const SCARED: u8 = 2;
|
||||
pub const UNROLLING: u8 = 3;
|
||||
|
||||
const ROLL_OUT: usize = 0;
|
||||
const ROLL_UP: usize = 1;
|
||||
const PEEK: usize = 2;
|
||||
|
||||
/// `ArmadilloState.animationDuration`.
|
||||
fn animation_duration(state: u8) -> i32 {
|
||||
match state {
|
||||
ROLLING => 10,
|
||||
SCARED => 50,
|
||||
UNROLLING => 30,
|
||||
_ => 0,
|
||||
}
|
||||
}
|
||||
|
||||
/// `Armadillo.ARMADILLO_STATE` and the brain's memory and behaviour.
|
||||
#[derive(Clone, Debug, Default)]
|
||||
pub struct State {
|
||||
pub state: u8,
|
||||
/// `inStateTicks`.
|
||||
pub in_state_ticks: i32,
|
||||
/// `DANGER_DETECTED_RECENTLY`: ticks to live, 0 when absent.
|
||||
danger: i32,
|
||||
/// Ticks to the sensor's next scan.
|
||||
sensor: i32,
|
||||
/// `ArmadilloBallUp`: running, its peek timer and whether it saw the
|
||||
/// danger around last tick.
|
||||
ball_up: bool,
|
||||
next_peek: i32,
|
||||
danger_was_around: bool,
|
||||
/// Whether the PANIC activity was the active one after the last tick.
|
||||
panic_activity: bool,
|
||||
/// `peekReceivedClient`.
|
||||
peek_received: bool,
|
||||
}
|
||||
|
||||
pub fn register(m: &mut Mob) {
|
||||
let mut panic = Panic::new(2.0);
|
||||
panic.environmental = true;
|
||||
let g = &mut m.ai.goals;
|
||||
g.add(0, Float);
|
||||
g.add(1, ArmadilloPanic(panic));
|
||||
g.add(2, Rolled);
|
||||
g.add(5, Stroll::avoiding_water(1.0));
|
||||
g.add(6, LookAtPlayer::new(6.0));
|
||||
g.add(7, RandomLookAround::default());
|
||||
}
|
||||
|
||||
/// `Armadillo.isScared`.
|
||||
pub fn scared(m: &Mob) -> bool {
|
||||
m.extra.armadillo.state != IDLE
|
||||
}
|
||||
/// `ArmadilloState.shouldHideInShell`: the model shows its shell.
|
||||
pub fn hiding_in_shell(m: &Mob) -> bool {
|
||||
let a = &m.extra.armadillo;
|
||||
match a.state {
|
||||
ROLLING => a.in_state_ticks > 5,
|
||||
SCARED => true,
|
||||
UNROLLING => a.in_state_ticks < 26,
|
||||
_ => false,
|
||||
}
|
||||
}
|
||||
/// `Armadillo.getState`, as it is synched.
|
||||
pub fn state(m: &Mob) -> u8 {
|
||||
m.extra.armadillo.state
|
||||
}
|
||||
/// `Armadillo.switchToState` and `onSyncedDataUpdated(ARMADILLO_STATE)`.
|
||||
pub fn set_state(m: &mut Mob, state: u8) {
|
||||
let a = &mut m.extra.armadillo;
|
||||
if a.state != state {
|
||||
a.state = state;
|
||||
a.in_state_ticks = 0;
|
||||
}
|
||||
}
|
||||
/// `Armadillo.canStayRolledUp`.
|
||||
fn can_stay_rolled_up(m: &Mob) -> bool {
|
||||
!m.ai.panicking
|
||||
&& !m.fluids.in_water
|
||||
&& !m.fluids.in_lava
|
||||
&& m.extra.leash.is_none()
|
||||
&& m.extra.mount.is_none()
|
||||
&& m.extra.rider.is_none()
|
||||
}
|
||||
/// `Entity.playSound(sound)`.
|
||||
fn play(m: &Mob, events: &mut Vec<MobEvent>, sound: &'static str) {
|
||||
if !m.silent {
|
||||
let at = [m.position.x, m.position.y, m.position.z];
|
||||
events.push(MobEvent::Sound(sound, at, 1.0, 1.0));
|
||||
}
|
||||
}
|
||||
/// `Armadillo.rollUp`.
|
||||
fn roll_up(m: &mut Mob, events: &mut Vec<MobEvent>) {
|
||||
if !scared(m) {
|
||||
super::creaking::stop_in_place(m);
|
||||
m.extra.in_love = 0;
|
||||
if !m.silent {
|
||||
m.voice("entity.armadillo.roll", events);
|
||||
}
|
||||
set_state(m, ROLLING);
|
||||
}
|
||||
}
|
||||
/// `Armadillo.rollOut`.
|
||||
fn roll_out(m: &mut Mob, events: &mut Vec<MobEvent>) {
|
||||
if scared(m) {
|
||||
if !m.silent {
|
||||
m.voice("entity.armadillo.unroll_finish", events);
|
||||
}
|
||||
set_state(m, IDLE);
|
||||
}
|
||||
}
|
||||
|
||||
/// `Armadillo.hurtServer`: a ball takes half of the damage past the first
|
||||
/// point.
|
||||
pub fn reduce_damage(m: &Mob, amount: f32) -> f32 {
|
||||
if scared(m) {
|
||||
((amount - 1.0) / 2.0).max(0.0)
|
||||
} else {
|
||||
amount
|
||||
}
|
||||
}
|
||||
/// `Armadillo.actuallyHurt`: a hit by a living thing curls it up (and keeps
|
||||
/// the danger in mind for 80 ticks), a hit by fire and the like unrolls it.
|
||||
pub fn after_hurt(m: &mut Mob, source: &Damage) {
|
||||
if m.no_ai || m.health <= 0.0 {
|
||||
return;
|
||||
}
|
||||
let mut events = Vec::new();
|
||||
if source.attacker.is_some() {
|
||||
m.extra.armadillo.danger = 80;
|
||||
if can_stay_rolled_up(m) {
|
||||
roll_up(m, &mut events);
|
||||
}
|
||||
} else if environmental(source.kind) {
|
||||
roll_out(m, &mut events);
|
||||
}
|
||||
m.outbox.append(&mut events);
|
||||
}
|
||||
/// `DamageTypeTags.PANIC_ENVIRONMENTAL_CAUSES`.
|
||||
pub fn environmental(kind: DamageKind) -> bool {
|
||||
matches!(
|
||||
kind,
|
||||
DamageKind::Cactus | DamageKind::OnFire | DamageKind::InFire | DamageKind::Lava
|
||||
)
|
||||
}
|
||||
/// The hurt sound: a ball's is muffled, and it has no ambient sound
|
||||
/// (`getAmbientSound`, `getHurtSound`); empty is none.
|
||||
pub fn sounds(m: &Mob) -> [&'static str; 3] {
|
||||
let [ambient, hurt, death] = m.kind.sounds;
|
||||
if scared(m) {
|
||||
["", "entity.armadillo.hurt_reduced", death]
|
||||
} else {
|
||||
[ambient, hurt, death]
|
||||
}
|
||||
}
|
||||
|
||||
/// `EntityTypeTags.UNDEAD`.
|
||||
fn undead(s: Species) -> bool {
|
||||
matches!(
|
||||
s,
|
||||
Species::Skeleton
|
||||
| Species::Stray
|
||||
| Species::WitherSkeleton
|
||||
| Species::SkeletonHorse
|
||||
| Species::Bogged
|
||||
| Species::Parched
|
||||
| Species::ZombieHorse
|
||||
| Species::CamelHusk
|
||||
| Species::Zombie
|
||||
| Species::ZombieVillager
|
||||
| Species::ZombifiedPiglin
|
||||
| Species::Zoglin
|
||||
| Species::Drowned
|
||||
| Species::Husk
|
||||
| Species::ZombieNautilus
|
||||
| Species::Wither
|
||||
| Species::Phantom
|
||||
)
|
||||
}
|
||||
/// `Armadillo.isScaredBy` for the nearby living entities: within 7 blocks
|
||||
/// sideways and 2 up and down, an undead mob, the last attacker, or a
|
||||
/// player who sprints or rides.
|
||||
fn scared_by_someone(m: &Mob, w: &MobWorld) -> bool {
|
||||
let area = m.bounds().inflate(Vec3::new(7.0, 2.0, 7.0));
|
||||
let attacker =
|
||||
m.ai.last_hurt_by
|
||||
.filter(|_| m.age as i32 - m.ai.last_hurt_by_time <= 100);
|
||||
if let Some(p) = &w.player
|
||||
&& p.alive
|
||||
&& !p.spectator
|
||||
&& p.bounds.intersects(area)
|
||||
&& (attacker == Some(Who::Player) || p.sprinting || p.riding)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
w.others_near(area).any(|s| {
|
||||
s.id != m.id
|
||||
&& s.alive
|
||||
&& s.bounds.intersects(area)
|
||||
&& (undead(s.species) || attacker == Some(Who::Mob(s.id)))
|
||||
})
|
||||
}
|
||||
|
||||
/// `Armadillo.customServerAiStep`'s brain: memories expire, the scare
|
||||
/// sensor scans every fifth tick, the core's rolling out, then the
|
||||
/// `ArmadilloBallUp` behaviour of the PANIC activity.
|
||||
pub fn server_ai_step(m: &mut Mob, cx: &mut Cx) {
|
||||
let events = &mut *cx.events;
|
||||
let a = &mut m.extra.armadillo;
|
||||
if a.danger > 0 {
|
||||
a.danger -= 1;
|
||||
}
|
||||
a.sensor -= 1;
|
||||
if a.sensor <= 0 {
|
||||
a.sensor = 5;
|
||||
if !can_stay_rolled_up(m) {
|
||||
m.extra.armadillo.danger = 0;
|
||||
} else if scared_by_someone(m, cx.w) {
|
||||
m.extra.armadillo.danger = 80;
|
||||
}
|
||||
}
|
||||
// ARMADILLO_ROLLING_OUT: no danger left in mind.
|
||||
if m.extra.armadillo.danger == 0 && scared(m) {
|
||||
roll_out(m, events);
|
||||
}
|
||||
if m.extra.armadillo.panic_activity && !m.extra.armadillo.ball_up && m.on_ground && !liquid(m) {
|
||||
// ArmadilloBallUp.start.
|
||||
m.extra.armadillo.ball_up = true;
|
||||
roll_up(m, events);
|
||||
}
|
||||
if m.extra.armadillo.ball_up {
|
||||
ball_up_tick(m, events);
|
||||
}
|
||||
let a = &mut m.extra.armadillo;
|
||||
a.panic_activity = a.danger > 0 && !m.ai.panicking;
|
||||
}
|
||||
fn liquid(m: &Mob) -> bool {
|
||||
m.fluids.in_water || m.fluids.in_lava
|
||||
}
|
||||
/// `ArmadilloBallUp.tickOrStop`.
|
||||
fn ball_up_tick(m: &mut Mob, events: &mut Vec<MobEvent>) {
|
||||
if !scared(m) {
|
||||
// canStillUse is false: stop.
|
||||
m.extra.armadillo.ball_up = false;
|
||||
if !can_stay_rolled_up(m) {
|
||||
roll_out(m, events);
|
||||
}
|
||||
return;
|
||||
}
|
||||
let a = &mut m.extra.armadillo;
|
||||
if a.next_peek > 0 {
|
||||
a.next_peek -= 1;
|
||||
}
|
||||
if a.state == ROLLING && a.in_state_ticks > animation_duration(ROLLING) {
|
||||
set_state(m, SCARED);
|
||||
if m.on_ground {
|
||||
play(m, events, "entity.armadillo.land");
|
||||
}
|
||||
return;
|
||||
}
|
||||
let danger = m.extra.armadillo.danger;
|
||||
let around = danger > 75;
|
||||
if around != m.extra.armadillo.danger_was_around {
|
||||
m.extra.armadillo.next_peek = next_peek_timer(m);
|
||||
}
|
||||
m.extra.armadillo.danger_was_around = around;
|
||||
let state = m.extra.armadillo.state;
|
||||
if state == SCARED {
|
||||
if m.extra.armadillo.next_peek == 0 && m.on_ground && around {
|
||||
peek(m, events);
|
||||
m.extra.armadillo.next_peek = next_peek_timer(m);
|
||||
}
|
||||
if danger < animation_duration(UNROLLING) {
|
||||
play(m, events, "entity.armadillo.unroll_start");
|
||||
set_state(m, UNROLLING);
|
||||
}
|
||||
} else if state == UNROLLING && danger > animation_duration(UNROLLING) {
|
||||
set_state(m, SCARED);
|
||||
}
|
||||
}
|
||||
/// `ArmadilloBallUp.pickNextPeekTimer`.
|
||||
fn next_peek_timer(m: &mut Mob) -> i32 {
|
||||
animation_duration(SCARED) + m.random.next_int_between_inclusive(100, 400).unwrap_or(100)
|
||||
}
|
||||
/// `level.broadcastEntityEvent(body, 64)`; the sound is the client's
|
||||
/// `playLocalSound`, which the server plays for everyone here.
|
||||
fn peek(m: &mut Mob, events: &mut Vec<MobEvent>) {
|
||||
entity_event(m, 64);
|
||||
let at = [m.position.x, m.position.y, m.position.z];
|
||||
events.push(MobEvent::EntityEvent {
|
||||
mob: m.id,
|
||||
event: 64,
|
||||
at,
|
||||
});
|
||||
events.push(MobEvent::Sound("entity.armadillo.peek", at, 1.0, 1.0));
|
||||
}
|
||||
/// `Armadillo.handleEntityEvent`: 64 is a peek.
|
||||
pub fn entity_event(m: &mut Mob, event: u8) {
|
||||
if event == 64 {
|
||||
m.extra.armadillo.peek_received = true;
|
||||
}
|
||||
}
|
||||
|
||||
/// `Armadillo.tick`: the animation states on a client, the ball's head held
|
||||
/// to its body, and `inStateTicks`. Server and client both run it.
|
||||
pub fn tick(m: &mut Mob) {
|
||||
animate(m);
|
||||
if scared(m) {
|
||||
// clampHeadRotationToBody with getMaxHeadYRot 0.
|
||||
m.head_yaw += super::living::wrap(m.body_yaw - m.head_yaw);
|
||||
}
|
||||
m.extra.armadillo.in_state_ticks += 1;
|
||||
}
|
||||
/// `Armadillo.setupAnimationStates`.
|
||||
fn animate(m: &mut Mob) {
|
||||
let age = m.age as f32;
|
||||
let a = &mut m.extra.armadillo;
|
||||
let s = &mut m.anim.states;
|
||||
match a.state {
|
||||
ROLLING => {
|
||||
s[ROLL_OUT].stop();
|
||||
s[ROLL_UP].start_if_stopped(age);
|
||||
s[PEEK].stop();
|
||||
}
|
||||
SCARED => {
|
||||
s[ROLL_OUT].stop();
|
||||
s[ROLL_UP].stop();
|
||||
if a.peek_received {
|
||||
s[PEEK].stop();
|
||||
a.peek_received = false;
|
||||
}
|
||||
if a.in_state_ticks == 0 {
|
||||
s[PEEK].start(age);
|
||||
s[PEEK].fast_forward(animation_duration(SCARED));
|
||||
} else {
|
||||
s[PEEK].start_if_stopped(age);
|
||||
}
|
||||
}
|
||||
UNROLLING => {
|
||||
s[ROLL_OUT].start_if_stopped(age);
|
||||
s[ROLL_UP].stop();
|
||||
s[PEEK].stop();
|
||||
}
|
||||
_ => {
|
||||
s[ROLL_OUT].stop();
|
||||
s[ROLL_UP].stop();
|
||||
s[PEEK].stop();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// `ArmadilloAi.ArmadilloPanic`: it unrolls, then runs.
|
||||
#[derive(Clone, Debug)]
|
||||
struct ArmadilloPanic(Panic);
|
||||
impl Goal for ArmadilloPanic {
|
||||
goal_clone!();
|
||||
fn flags(&self) -> u8 {
|
||||
self.0.flags()
|
||||
}
|
||||
fn name(&self) -> &'static str {
|
||||
"Panic"
|
||||
}
|
||||
fn can_use(&mut self, m: &mut Mob, cx: &mut Cx) -> bool {
|
||||
self.0.can_use(m, cx)
|
||||
}
|
||||
fn can_continue(&mut self, m: &mut Mob, cx: &mut Cx) -> bool {
|
||||
self.0.can_continue(m, cx)
|
||||
}
|
||||
fn start(&mut self, m: &mut Mob, cx: &mut Cx) {
|
||||
roll_out(m, cx.events);
|
||||
self.0.start(m, cx);
|
||||
}
|
||||
fn stop(&mut self, m: &mut Mob, cx: &mut Cx) {
|
||||
self.0.stop(m, cx);
|
||||
}
|
||||
}
|
||||
/// The behaviours that do not run while the armadillo is a ball: its
|
||||
/// `MoveToTargetSink` and the idle activity's walking and looking.
|
||||
#[derive(Clone, Debug)]
|
||||
struct Rolled;
|
||||
impl Goal for Rolled {
|
||||
goal_clone!();
|
||||
fn flags(&self) -> u8 {
|
||||
MOVE | LOOK
|
||||
}
|
||||
fn can_use(&mut self, m: &mut Mob, _cx: &mut Cx) -> bool {
|
||||
scared(m)
|
||||
}
|
||||
}
|
||||
@@ -59,11 +59,11 @@ pub struct Goat {
|
||||
pub ramming_x_head_rot: f32,
|
||||
}
|
||||
impl Goat {
|
||||
pub fn from_mob(_m: &Mob) -> Self {
|
||||
pub fn from_mob(m: &Mob) -> Self {
|
||||
Self {
|
||||
left_horn: true,
|
||||
right_horn: true,
|
||||
ramming_x_head_rot: 0.0,
|
||||
left_horn: !m.extra.lost_horns[0],
|
||||
right_horn: !m.extra.lost_horns[1],
|
||||
ramming_x_head_rot: super::goat::ramming_x_head_rot(m),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -960,7 +960,7 @@ pub fn anim(m: &Mob, s: &State, p: &mut Poses, a: &Animations) -> bool {
|
||||
match m.species() {
|
||||
Species::Goat => goat(p, s, &Goat::from_mob(m)),
|
||||
Species::PolarBear => polar_bear(p, s, super::polar_bear::standing_scale(m, s.partial)),
|
||||
Species::Panda => panda(p, s, &Panda::default()),
|
||||
Species::Panda => panda(p, s, &super::panda::pose(m, s.partial)),
|
||||
Species::Llama | Species::TraderLlama => {
|
||||
llama(p, s, m.extra.chested && !s.baby);
|
||||
}
|
||||
@@ -980,9 +980,10 @@ pub fn anim(m: &Mob, s: &State, p: &mut Poses, a: &Animations) -> bool {
|
||||
Species::Parrot => {
|
||||
let flap = m.anim.flap_o + (m.anim.flap - m.anim.flap_o) * s.partial;
|
||||
let speed = m.anim.flap_speed_o + (m.anim.flap_speed - m.anim.flap_speed_o) * s.partial;
|
||||
// `Parrot.isFlying` is `!onGround`, in water too.
|
||||
let pose = if m.extra.sitting {
|
||||
ParrotPose::Sitting
|
||||
} else if !m.on_ground && !m.fluids.in_water {
|
||||
} else if !m.on_ground {
|
||||
ParrotPose::Flying
|
||||
} else {
|
||||
ParrotPose::Standing
|
||||
@@ -1007,7 +1008,7 @@ pub fn anim(m: &Mob, s: &State, p: &mut Poses, a: &Animations) -> bool {
|
||||
p,
|
||||
s,
|
||||
&Armadillo {
|
||||
hiding_in_shell: false,
|
||||
hiding_in_shell: super::armadillo::hiding_in_shell(m),
|
||||
roll_out: st[0],
|
||||
roll_up: st[1],
|
||||
peek: st[2],
|
||||
@@ -1028,7 +1029,7 @@ pub fn anim(m: &Mob, s: &State, p: &mut Poses, a: &Animations) -> bool {
|
||||
a,
|
||||
),
|
||||
Species::Rabbit => rabbit(p, s, &st[0], &st[1], a),
|
||||
Species::Fox => fox(p, s, &Fox::default(), a),
|
||||
Species::Fox => fox(p, s, &super::fox::pose(m, s.partial), a),
|
||||
_ => return false,
|
||||
}
|
||||
true
|
||||
|
||||
@@ -31,6 +31,28 @@ pub fn pose(m: &Mob) -> u8 {
|
||||
pub(super) fn set_pose(m: &mut Mob, pose: u8) {
|
||||
m.extra.variant3 = pose;
|
||||
}
|
||||
/// `Pose.id` of the pose (`sync::pose_id`), and its inverse.
|
||||
pub fn pose_id(m: &Mob) -> u8 {
|
||||
match pose(m) {
|
||||
SHOOTING => 16,
|
||||
INHALING => 17,
|
||||
LONG_JUMPING => 6,
|
||||
SLIDING => 15,
|
||||
_ => 0,
|
||||
}
|
||||
}
|
||||
pub(super) fn set_pose_id(m: &mut Mob, id: u8) {
|
||||
set_pose(
|
||||
m,
|
||||
match id {
|
||||
16 => SHOOTING,
|
||||
17 => INHALING,
|
||||
6 => LONG_JUMPING,
|
||||
15 => SLIDING,
|
||||
_ => STANDING,
|
||||
},
|
||||
);
|
||||
}
|
||||
/// `Breeze.getFiringYPosition`.
|
||||
pub fn firing_y(m: &Mob) -> f64 {
|
||||
m.position.y + f64::from(m.height()) / 2.0 + f64::from(0.3_f32)
|
||||
|
||||
@@ -45,6 +45,25 @@ pub fn sitting(m: &Mob) -> bool {
|
||||
fn in_transition(m: &Mob) -> bool {
|
||||
pose_time(m) < if sitting(m) { SIT_DOWN } else { STAND_UP }
|
||||
}
|
||||
/// `Camel.getPoseTime` of a client mob from the `LAST_POSE_CHANGE_TICK` it
|
||||
/// was sent and the client's game time, and whether that value says it sits.
|
||||
fn client_pose(m: &Mob, game_time: i64) -> (i64, bool) {
|
||||
let tick = m.extra.pose_change_tick;
|
||||
(game_time - tick.abs(), tick < 0)
|
||||
}
|
||||
/// The camel's `LAST_POSE_CHANGE_TICK` as the server sends it: in game time
|
||||
/// (a mob counts its own age), negative while sitting.
|
||||
pub fn synced_pose_tick(m: &Mob, game_time: i64) -> i64 {
|
||||
let tick = (game_time - pose_time(m)).max(0);
|
||||
if sitting(m) { -tick.max(1) } else { tick }
|
||||
}
|
||||
/// `Entity.onSyncedDataUpdated` for the pose tick of a client mob: its sign
|
||||
/// is whether it sits. `client_tick` keeps `egg_time` (in the mob's age, for
|
||||
/// `sitting` and `refuse_to_move`) with it.
|
||||
pub fn set_synced_pose_tick(m: &mut Mob, tick: i64) {
|
||||
m.extra.pose_change_tick = tick;
|
||||
m.extra.egg_time = if tick < 0 { -1 } else { 0 };
|
||||
}
|
||||
/// `Camel.refuseToMove`: sitting or getting up or down.
|
||||
pub fn refuse_to_move(m: &Mob) -> bool {
|
||||
sitting(m) || in_transition(m)
|
||||
@@ -166,7 +185,29 @@ pub fn ai_step(m: &mut Mob, events: &mut Vec<MobEvent>) {
|
||||
m.ai.zza = 0.0;
|
||||
m.ai.nav.stop();
|
||||
}
|
||||
// setupAnimationStates.
|
||||
animation_states(m, pose_time(m), sitting(m));
|
||||
}
|
||||
/// `Camel.tick` on a client that is not the server: the dash cooldown, which
|
||||
/// the dash flag starts, and the keyframe states from the synced pose tick.
|
||||
pub fn client_tick(m: &mut Mob, game_time: i64) {
|
||||
if m.extra.anim_ticks > 0 {
|
||||
m.extra.anim_ticks -= 1;
|
||||
}
|
||||
let (pose_time, sitting) = client_pose(m, game_time);
|
||||
let since = (i64::from(m.age) - pose_time).max(0) as i32;
|
||||
m.extra.egg_time = if sitting { -since.max(1) } else { since };
|
||||
animation_states(m, pose_time, sitting);
|
||||
}
|
||||
/// `Camel.onSyncedDataUpdated(DASH)`: a dash starts the cooldown unless it
|
||||
/// runs. Not on the first tick.
|
||||
pub fn set_dashing(m: &mut Mob, dashing: bool) {
|
||||
if m.age > 0 && m.extra.anim_ticks == 0 {
|
||||
m.extra.anim_ticks = DASH_COOLDOWN;
|
||||
}
|
||||
m.extra.flag2 = dashing;
|
||||
}
|
||||
/// `Camel.setupAnimationStates` for a pose time and whether it sits.
|
||||
fn animation_states(m: &mut Mob, pose_time: i64, sitting: bool) {
|
||||
let age = m.age as f32;
|
||||
if m.extra.ints[0] <= 0 {
|
||||
m.extra.ints[0] = m.random.next_int_bound(40).unwrap_or(0) + 80;
|
||||
@@ -175,9 +216,9 @@ pub fn ai_step(m: &mut Mob, events: &mut Vec<MobEvent>) {
|
||||
m.extra.ints[0] -= 1;
|
||||
}
|
||||
// isCamelVisuallySitting: (poseTime < 0) != sitting.
|
||||
let visually_sitting = (pose_time(m) < 0) != sitting(m);
|
||||
let sitting_down = sitting(m) && pose_time(m) < SIT_DOWN && pose_time(m) >= 0;
|
||||
let transition = in_transition(m) && pose_time(m) >= 0;
|
||||
let visually_sitting = (pose_time < 0) != sitting;
|
||||
let sitting_down = sitting && (0..SIT_DOWN).contains(&pose_time);
|
||||
let transition = (0..if sitting { SIT_DOWN } else { STAND_UP }).contains(&pose_time);
|
||||
let dashing = m.extra.flag2;
|
||||
let st = &mut m.anim.states;
|
||||
if visually_sitting {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
//! Mob controls: `MoveControl` (with the flying and smooth-swimming
|
||||
//! variants), `LookControl`, `JumpControl` and `BodyRotationControl`.
|
||||
use super::{Mob, MobWorld, living::wrap};
|
||||
use super::{Mob, MobWorld, Species, living::wrap};
|
||||
use horizon_core::geometry::trig;
|
||||
|
||||
/// `2.5000003E-7F` widened: the squared distance controls treat as arrived.
|
||||
@@ -113,8 +113,16 @@ pub struct BodyRotation {
|
||||
last_stable_head: f32,
|
||||
}
|
||||
impl Mob {
|
||||
/// `Mob.getMaxHeadXRot`, with `Wolf`'s 20 degrees while sitting.
|
||||
pub fn max_head_x_rot(&self) -> f32 {
|
||||
if self.species() == Species::Wolf && self.extra.sitting {
|
||||
20.0
|
||||
} else {
|
||||
self.kind.max_head_x_rot
|
||||
}
|
||||
}
|
||||
pub fn look_at(&mut self, at: [f64; 3]) {
|
||||
let (y, x) = (self.kind.head_rot_speed, self.kind.max_head_x_rot);
|
||||
let (y, x) = (self.kind.head_rot_speed, self.max_head_x_rot());
|
||||
self.ai.look.set_look_at(at, y, x);
|
||||
}
|
||||
pub fn look_at_speed(&mut self, at: [f64; 3], y_speed: f32, x_max: f32) {
|
||||
@@ -135,6 +143,16 @@ impl Mob {
|
||||
if self.species() == super::Species::Rabbit {
|
||||
return super::rabbit::move_control(self, w);
|
||||
}
|
||||
// Fox.FoxMoveControl.tick: still while asleep, sitting or faceplanted.
|
||||
if self.species() == super::Species::Fox && !super::fox::can_move(self) {
|
||||
return;
|
||||
}
|
||||
// Panda.PandaMoveControl: only while it can act.
|
||||
if self.species() == super::Species::Panda
|
||||
&& !super::panda::can_perform_action(self, w.thundering)
|
||||
{
|
||||
return;
|
||||
}
|
||||
let attr_speed = self.movement_speed();
|
||||
match self.ai.mov.kind {
|
||||
MoveKind::Custom => super::ai::custom_move_control(self, w, events),
|
||||
@@ -314,6 +332,11 @@ impl Mob {
|
||||
/// LookControl.tick (and SmoothSwimmingLookControl).
|
||||
pub fn look_control_tick(&mut self) {
|
||||
let look = self.ai.look.clone();
|
||||
let fox = self.species() == super::Species::Fox;
|
||||
// Fox.FoxLookControl: no turning in sleep.
|
||||
if fox && super::fox::flag(self, super::fox::SLEEPING) {
|
||||
return;
|
||||
}
|
||||
if look.kind == LookKind::HappyGhast {
|
||||
return super::happy_ghast::look_control(self);
|
||||
}
|
||||
@@ -346,7 +369,8 @@ impl Mob {
|
||||
}
|
||||
}
|
||||
LookKind::Default | LookKind::Fixed => {
|
||||
if look.kind == LookKind::Default {
|
||||
// FoxLookControl.resetXRotOnTick keeps a stalking or pouncing pitch.
|
||||
if look.kind == LookKind::Default && (!fox || super::fox::resets_x_rot(self)) {
|
||||
self.head_pitch = 0.0;
|
||||
}
|
||||
if look.cooldown > 0 {
|
||||
|
||||
@@ -70,6 +70,16 @@ pub fn sound(m: &Mob, what: &str) -> &'static str {
|
||||
}
|
||||
/// `CopperGolem.setupAnimationStates` (client) and the sound of the head spin.
|
||||
pub fn ai_step(m: &mut Mob, _w: &MobWorld, events: &mut Vec<MobEvent>) {
|
||||
if animate(m) {
|
||||
let at = [m.position.x, m.position.y, m.position.z];
|
||||
events.push(MobEvent::LocalSound(sound(m, "spin"), at, 1.0, 1.0));
|
||||
}
|
||||
}
|
||||
/// `CopperGolem.setupAnimationStates`, for the server and a client alike;
|
||||
/// returns whether the head spin sound plays now (a client's sound is the
|
||||
/// server's `LocalSound`).
|
||||
pub fn animate(m: &mut Mob) -> bool {
|
||||
let mut spin = false;
|
||||
let age = m.age as f32;
|
||||
let state = m.extra.variant3;
|
||||
let st = &mut m.anim.states;
|
||||
@@ -93,8 +103,7 @@ pub fn ai_step(m: &mut Mob, _w: &MobWorld, events: &mut Vec<MobEvent>) {
|
||||
m.extra.ints[0] = tick + 200 + r.next_int_bound(40).unwrap_or(0);
|
||||
}
|
||||
if tick == m.extra.ints[0] + 10 {
|
||||
let at = [m.position.x, m.position.y, m.position.z];
|
||||
events.push(MobEvent::LocalSound(sound(m, "spin"), at, 1.0, 1.0));
|
||||
spin = true;
|
||||
m.extra.ints[0] = 0;
|
||||
}
|
||||
}
|
||||
@@ -106,6 +115,7 @@ pub fn ai_step(m: &mut Mob, _w: &MobWorld, events: &mut Vec<MobEvent>) {
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
spin
|
||||
}
|
||||
/// `CopperGolem.updateWeathering` on the level random: the first tick sets
|
||||
/// the next weathering tick, which advances the weather state; once
|
||||
|
||||
@@ -152,8 +152,26 @@ pub fn sway(m: &mut Mob, events: &mut Vec<MobEvent>) {
|
||||
}
|
||||
/// `Creaking.doHurtTarget`: the attack animation (the sound is the
|
||||
/// `playAttackSound` that ends `Mob.doHurtTarget`).
|
||||
pub fn on_attack(m: &mut Mob, _events: &mut Vec<MobEvent>) {
|
||||
pub fn on_attack(m: &mut Mob, events: &mut Vec<MobEvent>) {
|
||||
m.extra.ints[ATTACKING] = ATTACK_TICKS;
|
||||
broadcast(m, 4, events);
|
||||
}
|
||||
/// `Level.broadcastEntityEvent` for a creaking (guests run `entity_event`).
|
||||
fn broadcast(m: &Mob, event: i8, events: &mut Vec<MobEvent>) {
|
||||
events.push(MobEvent::EntityEvent {
|
||||
mob: m.id,
|
||||
event,
|
||||
at: [m.position.x, m.position.y, m.position.z],
|
||||
});
|
||||
}
|
||||
/// `Creaking.handleEntityEvent` on a client: 66 the invulnerability flash,
|
||||
/// 4 the attack swing (the sounds are the server's).
|
||||
pub fn entity_event(m: &mut Mob, event: u8) {
|
||||
match event {
|
||||
66 => m.extra.ints[INVULNERABLE] = INVULNERABILITY_TICKS,
|
||||
4 => m.extra.ints[ATTACKING] = ATTACK_TICKS,
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
/// The heart this creaking is bound to protects it (`isProtector`); a link
|
||||
/// read from a save is trusted until the heart resolves it.
|
||||
@@ -176,15 +194,12 @@ pub fn update(m: &mut Mob, w: &MobWorld, events: &mut Vec<MobEvent>) {
|
||||
if heart_bound(m) && m.alive() && !protected(m, w) {
|
||||
m.health = 0.0;
|
||||
}
|
||||
for slot in [INVULNERABLE, ATTACKING] {
|
||||
if m.extra.ints[slot] > 0 {
|
||||
m.extra.ints[slot] -= 1;
|
||||
}
|
||||
// hurtServer set the invulnerability clock since the last tick: entity
|
||||
// event 66.
|
||||
if m.extra.ints[INVULNERABLE] == INVULNERABILITY_TICKS {
|
||||
broadcast(m, 66, events);
|
||||
}
|
||||
let age = m.age as f32;
|
||||
m.anim.states[0].animate_when(m.extra.ints[ATTACKING] > 0, age);
|
||||
m.anim.states[1].animate_when(m.extra.ints[INVULNERABLE] > 0, age);
|
||||
m.anim.states[2].animate_when(tearing_down(m), age);
|
||||
animate(m);
|
||||
if m.extra.counter & HURT != 0 {
|
||||
// hurtServer: sway, and the heart reacts to a player's hit.
|
||||
if protected(m, w) {
|
||||
@@ -200,18 +215,6 @@ pub fn update(m: &mut Mob, w: &MobWorld, events: &mut Vec<MobEvent>) {
|
||||
}
|
||||
m.extra.counter &= !(HURT | HURT_BY_PLAYER);
|
||||
}
|
||||
// checkEyeBlink: the eyes of a creaking falling apart flicker.
|
||||
if m.death_time > m.extra.ints[NEXT_FLICKER] {
|
||||
let glowing = m.extra.ints[EYES] != 0;
|
||||
let (lo, hi) = if glowing {
|
||||
(2, 8)
|
||||
} else {
|
||||
(m.death_time / 4, m.death_time / 2)
|
||||
};
|
||||
let span = (hi - lo + 1).max(1);
|
||||
m.extra.ints[NEXT_FLICKER] = m.death_time + lo + m.random.next_int_bound(span).unwrap_or(0);
|
||||
m.extra.ints[EYES] = i32::from(!glowing);
|
||||
}
|
||||
if !m.alive() {
|
||||
return;
|
||||
}
|
||||
@@ -226,6 +229,31 @@ pub fn update(m: &mut Mob, w: &MobWorld, events: &mut Vec<MobEvent>) {
|
||||
m.extra.flag2 = !now;
|
||||
}
|
||||
}
|
||||
/// The clocks and keyframe states `Creaking.aiStep`, `setupAnimationStates`
|
||||
/// and `checkEyeBlink` run (the server's and a client's).
|
||||
pub fn animate(m: &mut Mob) {
|
||||
for slot in [INVULNERABLE, ATTACKING] {
|
||||
if m.extra.ints[slot] > 0 {
|
||||
m.extra.ints[slot] -= 1;
|
||||
}
|
||||
}
|
||||
let age = m.age as f32;
|
||||
m.anim.states[0].animate_when(m.extra.ints[ATTACKING] > 0, age);
|
||||
m.anim.states[1].animate_when(m.extra.ints[INVULNERABLE] > 0, age);
|
||||
m.anim.states[2].animate_when(tearing_down(m), age);
|
||||
// checkEyeBlink: the eyes of a creaking falling apart flicker.
|
||||
if m.death_time > m.extra.ints[NEXT_FLICKER] {
|
||||
let glowing = m.extra.ints[EYES] != 0;
|
||||
let (lo, hi) = if glowing {
|
||||
(2, 8)
|
||||
} else {
|
||||
(m.death_time / 4, m.death_time / 2)
|
||||
};
|
||||
let span = (hi - lo + 1).max(1);
|
||||
m.extra.ints[NEXT_FLICKER] = m.death_time + lo + m.random.next_int_bound(span).unwrap_or(0);
|
||||
m.extra.ints[EYES] = i32::from(!glowing);
|
||||
}
|
||||
}
|
||||
/// `Mob.stopInPlace`.
|
||||
pub fn stop_in_place(m: &mut Mob) {
|
||||
m.nav_stop();
|
||||
|
||||
@@ -244,7 +244,13 @@ pub fn slot_item(m: &Mob, slot: &str) -> Option<LootStack> {
|
||||
}
|
||||
_ => (None, None),
|
||||
};
|
||||
let mut stack = loot_stack(item?, 1);
|
||||
// A panda keeps the whole stack it picked up in its mouth.
|
||||
let count = if slot == "mainhand" && m.species() == Species::Panda {
|
||||
super::panda::held_count(m)
|
||||
} else {
|
||||
1
|
||||
};
|
||||
let mut stack = loot_stack(item?, count as i32);
|
||||
if let (Some(index), Some(g)) = (index, gear::gear(m)) {
|
||||
for (e, l) in &g.slots[index] {
|
||||
stack.enchantments.push((e.to_string(), *l as i32));
|
||||
|
||||
@@ -184,15 +184,7 @@ pub fn ai_step(m: &mut Mob, w: &MobWorld, events: &mut Vec<MobEvent>) {
|
||||
}
|
||||
}
|
||||
}
|
||||
if (m.fluids.in_water
|
||||
|| w.raining
|
||||
&& w.can_see_sky(
|
||||
m.position.x.floor() as i32,
|
||||
m.eye_y().floor() as i32,
|
||||
m.position.z.floor() as i32,
|
||||
))
|
||||
&& m.alive()
|
||||
{
|
||||
if (m.fluids.in_water || super::ai::in_rain(m, w)) && m.alive() {
|
||||
// Hurt by water: teleport away.
|
||||
m.extra.counter2 = m.extra.counter2.max(1);
|
||||
}
|
||||
|
||||
@@ -1677,7 +1677,7 @@ impl<L: FinalizeLevel + ?Sized, R: RandomSource> Ctx<'_, L, R> {
|
||||
let jump = self.uniform(600, 1200);
|
||||
set_memory(m, "long_jump_cooldown", i64::from(jump), i64::MAX);
|
||||
let ram = self.uniform(600, 6000);
|
||||
set_memory(m, "ram_cooldown", i64::from(ram), i64::MAX);
|
||||
m.extra.goat.ram_cooldown = ram;
|
||||
m.extra.screaming = self.random.next_double() < 0.02;
|
||||
age_boundary_reached(m);
|
||||
if !m.is_baby() && self.float() < 0.1 {
|
||||
@@ -1691,13 +1691,7 @@ impl<L: FinalizeLevel + ?Sized, R: RandomSource> Ctx<'_, L, R> {
|
||||
let hidden = panda_gene(self.int(16));
|
||||
m.extra.variant = main;
|
||||
m.extra.variant2 = hidden;
|
||||
let shown = panda_variant(main, hidden);
|
||||
if shown == 5 {
|
||||
set_base(m, Attr::MaxHealth, 10.0);
|
||||
}
|
||||
if shown == 1 {
|
||||
set_base(m, Attr::MovementSpeed, f64::from(0.07f32));
|
||||
}
|
||||
super::panda::set_attributes(m);
|
||||
self.ageable(m, g.or(Some(Group::Ageable(Ageable::chance(0.2)))))
|
||||
}
|
||||
S::PolarBear | S::Ocelot => {
|
||||
@@ -1950,7 +1944,7 @@ fn set_memory(m: &mut Mob, name: &'static str, value: i64, ttl: i64) {
|
||||
}
|
||||
/// `Panda.Gene.getRandom` (enum order: normal, lazy, worried, playful,
|
||||
/// brown, weak, aggressive).
|
||||
fn panda_gene(roll: i32) -> u8 {
|
||||
pub fn panda_gene(roll: i32) -> u8 {
|
||||
match roll {
|
||||
0 => 1,
|
||||
1 => 2,
|
||||
|
||||
@@ -0,0 +1,845 @@
|
||||
//! Foxes (`net.minecraft.world.entity.animal.fox.Fox`): the goal list, the
|
||||
//! flags byte (sleeping, sitting, crouching, interested, pouncing,
|
||||
//! faceplanted), stalking and pouncing, napping in the shade by day, seeking
|
||||
//! shelter, perching, eating berries and the food held in the mouth.
|
||||
//! Picking up items, strolling through villages and trusting players (with
|
||||
//! defending them) are not ported yet.
|
||||
use super::{
|
||||
Mob, MobEvent, MobWorld, Seen, Species,
|
||||
goals::{
|
||||
Avoid, AvoidEntity, Breed, Cx, Float, FollowParent, Goal, JUMP, LOOK, LeapAtTarget,
|
||||
LookAtPlayer, MOVE, MeleeAttack, MoveToBlock, NearestTarget, Panic, Prey, Stroll,
|
||||
goal_clone, hide_pos, reduced,
|
||||
},
|
||||
living::wrap,
|
||||
};
|
||||
use horizon_core::{
|
||||
geometry::{Axis, Vec3},
|
||||
jmath,
|
||||
random::RandomSource,
|
||||
};
|
||||
use horizon_world::registry::BlockStateId;
|
||||
|
||||
// Fox.DATA_FLAGS_ID, kept in `extra.ints[0]` (and sent to clients as is).
|
||||
pub const SITTING: i32 = 1;
|
||||
pub const CROUCHING: i32 = 4;
|
||||
pub const INTERESTED: i32 = 8;
|
||||
pub const POUNCING: i32 = 16;
|
||||
pub const SLEEPING: i32 = 32;
|
||||
pub const FACEPLANTED: i32 = 64;
|
||||
pub const DEFENDING: i32 = 128;
|
||||
/// `Fox.ticksSinceEaten`.
|
||||
const TICKS_SINCE_EATEN: usize = 1;
|
||||
/// `Fox.interestedAngle` and `crouchAmount` in `anim.v`, the previous tick's
|
||||
/// values in `anim.vo`.
|
||||
const INTERESTED_ANGLE: usize = 0;
|
||||
const CROUCH_AMOUNT: usize = 1;
|
||||
/// `Fox.MAX_CROUCH_AMOUNT`.
|
||||
const MAX_CROUCH_AMOUNT: f32 = 5.0;
|
||||
|
||||
pub fn flag(m: &Mob, f: i32) -> bool {
|
||||
m.extra.ints[0] & f != 0
|
||||
}
|
||||
fn set_flag(m: &mut Mob, f: i32, on: bool) {
|
||||
if on {
|
||||
m.extra.ints[0] |= f;
|
||||
} else {
|
||||
m.extra.ints[0] &= !f;
|
||||
}
|
||||
}
|
||||
/// `Fox.clearStates`.
|
||||
fn clear_states(m: &mut Mob) {
|
||||
m.extra.ints[0] = 0;
|
||||
}
|
||||
/// `Fox.canMove`, which `FoxMoveControl` waits for.
|
||||
pub fn can_move(m: &Mob) -> bool {
|
||||
!flag(m, SLEEPING | SITTING | FACEPLANTED)
|
||||
}
|
||||
/// `FoxLookControl.resetXRotOnTick`: stalking and pouncing keep their pitch.
|
||||
pub fn resets_x_rot(m: &Mob) -> bool {
|
||||
!flag(m, POUNCING | CROUCHING | INTERESTED | FACEPLANTED)
|
||||
}
|
||||
|
||||
/// `Fox.registerGoals` and `setTargetGoals` (the targets wait for the variant:
|
||||
/// red foxes prefer chickens and rabbits, snow foxes fish).
|
||||
pub fn register(m: &mut Mob) {
|
||||
let countdown = m.random.next_int_bound(reduced(140)).unwrap_or(0);
|
||||
let g = &mut m.ai.goals;
|
||||
g.add(0, Wrap::new(Float).on_start(clear_states));
|
||||
g.add(1, Faceplant { countdown: 0 });
|
||||
g.add(2, Panic::new(2.2));
|
||||
g.add(3, Wrap::new(Breed::new(1.0)).on_start(clear_states));
|
||||
// AVOID_PLAYERS: not a sneaking player.
|
||||
g.add(
|
||||
4,
|
||||
AvoidEntity::new(Avoid::Player, 16.0, 1.6, 1.4)
|
||||
.player_when(|p| !p.sneaking && !p.spectator),
|
||||
);
|
||||
g.add(
|
||||
4,
|
||||
AvoidEntity::new(Avoid::Species(vec![Species::Wolf]), 8.0, 1.6, 1.4).seen_when(|s| !s.tame),
|
||||
);
|
||||
g.add(
|
||||
4,
|
||||
AvoidEntity::new(Avoid::Species(vec![Species::PolarBear]), 8.0, 1.6, 1.4),
|
||||
);
|
||||
g.add(5, StalkPrey);
|
||||
g.add(6, Pounce);
|
||||
g.add(6, SeekShelter::new(1.25));
|
||||
g.add(
|
||||
7,
|
||||
Wrap::new(MeleeAttack::new(1.2, true))
|
||||
.when(
|
||||
|m| !flag(m, SITTING | SLEEPING | CROUCHING | FACEPLANTED),
|
||||
false,
|
||||
)
|
||||
.on_start(|m| set_flag(m, INTERESTED, false)),
|
||||
);
|
||||
g.add(7, Sleep { countdown });
|
||||
g.add(8, Wrap::new(FollowParent::new(1.25)).on_start(clear_states));
|
||||
g.add(10, EatBerries::new());
|
||||
g.add(10, LeapAtTarget::new(0.4));
|
||||
g.add(11, Stroll::avoiding_water(1.0));
|
||||
// FoxLookAtPlayerGoal: not with the nose in the snow or while stalking.
|
||||
g.add(
|
||||
12,
|
||||
Wrap::new(LookAtPlayer::new(24.0)).when(|m| !flag(m, FACEPLANTED | INTERESTED), true),
|
||||
);
|
||||
g.add(13, Perch::default());
|
||||
let red = |m: &Mob, _: &MobWorld| m.extra.variant == 0;
|
||||
let snow = |m: &Mob, _: &MobWorld| m.extra.variant != 0;
|
||||
let land = || NearestTarget::new(Prey::Filter(chicken_or_rabbit), false).interval(10);
|
||||
let turtle = || NearestTarget::new(Prey::Filter(baby_turtle_on_land), false).interval(10);
|
||||
let fish = || NearestTarget::new(Prey::Filter(schooling_fish), false).interval(20);
|
||||
let t = &mut m.ai.targets;
|
||||
t.add(4, land().gate(red));
|
||||
t.add(4, turtle().gate(red));
|
||||
t.add(6, fish().gate(red));
|
||||
t.add(4, fish().gate(snow));
|
||||
t.add(6, land().gate(snow));
|
||||
t.add(6, turtle().gate(snow));
|
||||
}
|
||||
|
||||
fn chicken_or_rabbit(s: &Seen) -> bool {
|
||||
matches!(s.species, Species::Chicken | Species::Rabbit)
|
||||
}
|
||||
fn baby_turtle_on_land(s: &Seen) -> bool {
|
||||
s.species == Species::Turtle && s.baby && !s.in_water
|
||||
}
|
||||
fn schooling_fish(s: &Seen) -> bool {
|
||||
matches!(
|
||||
s.species,
|
||||
Species::Cod | Species::Salmon | Species::TropicalFish
|
||||
)
|
||||
}
|
||||
|
||||
/// The part of `Fox.tick` a client runs too: the head tilt eases toward the
|
||||
/// interested flag and the crouch builds up to 5 while crouching.
|
||||
pub fn client_tick(m: &mut Mob) {
|
||||
let (interested, crouching) = (flag(m, INTERESTED), flag(m, CROUCHING));
|
||||
let v = &mut m.anim.v;
|
||||
m.anim.vo[INTERESTED_ANGLE] = v[INTERESTED_ANGLE];
|
||||
v[INTERESTED_ANGLE] += (if interested { 1.0 } else { 0.0 } - v[INTERESTED_ANGLE]) * 0.4;
|
||||
m.anim.vo[CROUCH_AMOUNT] = v[CROUCH_AMOUNT];
|
||||
v[CROUCH_AMOUNT] = if crouching {
|
||||
(v[CROUCH_AMOUNT] + 0.2).min(MAX_CROUCH_AMOUNT)
|
||||
} else {
|
||||
0.0
|
||||
};
|
||||
}
|
||||
/// `Fox.isFullyCrouched`.
|
||||
fn fully_crouched(m: &Mob) -> bool {
|
||||
m.anim.v[CROUCH_AMOUNT] == MAX_CROUCH_AMOUNT
|
||||
}
|
||||
|
||||
/// `Fox.tick` and the eating part of `Fox.aiStep`.
|
||||
pub fn ai_step(m: &mut Mob, w: &MobWorld, events: &mut Vec<MobEvent>) {
|
||||
client_tick(m);
|
||||
if !m.alive() {
|
||||
return;
|
||||
}
|
||||
m.extra.ints[TICKS_SINCE_EATEN] += 1;
|
||||
if let Some(item) = m.extra.main_hand.clone()
|
||||
&& eats(w, &item)
|
||||
&& m.ai.target.is_none()
|
||||
&& m.on_ground
|
||||
&& !flag(m, SLEEPING)
|
||||
{
|
||||
let eaten = m.extra.ints[TICKS_SINCE_EATEN];
|
||||
if eaten > 600 {
|
||||
// ItemStack.finishUsingItem: the food is gone, a use remainder stays.
|
||||
m.extra.main_hand = w
|
||||
.items
|
||||
.and_then(|i| i.items.get(&item))
|
||||
.and_then(|i| i.use_remainder.as_deref())
|
||||
.map(Into::into);
|
||||
m.extra.ints[TICKS_SINCE_EATEN] = 0;
|
||||
} else if eaten > 560 && m.random.next_float() < 0.1 {
|
||||
let at = [m.position.x, m.position.y, m.position.z];
|
||||
events.push(MobEvent::Sound("entity.fox.eat", at, 1.0, 1.0));
|
||||
}
|
||||
}
|
||||
if !m.target_info(w).is_some_and(|t| t.alive) {
|
||||
set_flag(m, CROUCHING | INTERESTED, false);
|
||||
}
|
||||
let in_water = m.fluids.in_water;
|
||||
if in_water || m.ai.target.is_some() || w.thundering {
|
||||
set_flag(m, SLEEPING, false);
|
||||
}
|
||||
if in_water || flag(m, SLEEPING) {
|
||||
set_flag(m, SITTING, false);
|
||||
}
|
||||
// Faceplanted: crumbs of the snow now and then (level event 2001).
|
||||
if flag(m, FACEPLANTED) && m.random.next_float() < 0.2 {
|
||||
let at = [
|
||||
m.position.x.floor() as i32,
|
||||
m.position.y.floor() as i32,
|
||||
m.position.z.floor() as i32,
|
||||
];
|
||||
events.push(MobEvent::LevelEvent {
|
||||
id: 2001,
|
||||
pos: at,
|
||||
data: w.world.block(at[0], at[1], at[2]).0 as i32,
|
||||
});
|
||||
}
|
||||
}
|
||||
/// `Fox.aiStep` before the living step: a sleeping fox takes no input.
|
||||
pub fn pre_ai_step(m: &mut Mob) {
|
||||
if flag(m, SLEEPING) {
|
||||
m.ai.jumping = false;
|
||||
m.ai.xxa = 0.0;
|
||||
m.ai.zza = 0.0;
|
||||
}
|
||||
}
|
||||
/// `Fox.isConsumableFood`: the item has food and consumable components.
|
||||
fn eats(w: &MobWorld, item: &str) -> bool {
|
||||
w.items
|
||||
.and_then(|i| i.items.get(item))
|
||||
.is_some_and(|i| i.food.is_some() && i.consumable.is_some())
|
||||
}
|
||||
|
||||
/// `Fox.FoxBehaviorGoal.hasShelter`: out of the sky's sight and not a bad spot.
|
||||
fn has_shelter(m: &Mob, w: &MobWorld) -> bool {
|
||||
let at = [
|
||||
m.position.x.floor() as i32,
|
||||
m.bounds().max(Axis::Y).floor() as i32,
|
||||
m.position.z.floor() as i32,
|
||||
];
|
||||
!w.can_see_sky(at[0], at[1], at[2]) && m.walk_target_value(w, at) >= 0.0
|
||||
}
|
||||
/// `Fox.FoxBehaviorGoal.alertable`: someone within 12 blocks the fox would
|
||||
/// keep watch over (`FoxAlertableEntitiesSelector`).
|
||||
fn alertable(m: &Mob, w: &MobWorld) -> bool {
|
||||
let (from, id, y) = (m.position, m.id, m.position.y);
|
||||
let mob = w
|
||||
.nearby(from, 12.0, |s: &Seen| {
|
||||
s.id != id
|
||||
&& (s.position.y - y).abs() <= 6.0
|
||||
&& m.distance_sqr(s.position) <= 144.0
|
||||
&& match s.species {
|
||||
Species::Fox => false,
|
||||
Species::Chicken | Species::Rabbit => true,
|
||||
Species::Wolf | Species::Cat | Species::Parrot | Species::Nautilus => !s.tame,
|
||||
_ => true,
|
||||
}
|
||||
})
|
||||
.next()
|
||||
.is_some();
|
||||
mob || w.player.as_ref().is_some_and(|p| {
|
||||
p.alive
|
||||
&& !p.creative
|
||||
&& !p.spectator
|
||||
&& !p.sneaking
|
||||
&& (p.position.y - y).abs() <= 6.0
|
||||
&& m.distance_sqr(p.position) <= 144.0
|
||||
})
|
||||
}
|
||||
|
||||
/// `Fox.SleepGoal`: nap in the shade by day when nothing is around.
|
||||
#[derive(Clone, Debug)]
|
||||
struct Sleep {
|
||||
countdown: i32,
|
||||
}
|
||||
impl Sleep {
|
||||
fn can_sleep(&mut self, m: &Mob, w: &MobWorld) -> bool {
|
||||
if self.countdown > 0 {
|
||||
self.countdown -= 1;
|
||||
return false;
|
||||
}
|
||||
let (x, y, z) = (
|
||||
m.position.x.floor() as i32,
|
||||
m.position.y.floor() as i32,
|
||||
m.position.z.floor() as i32,
|
||||
);
|
||||
w.daylight
|
||||
&& has_shelter(m, w)
|
||||
&& !alertable(m, w)
|
||||
&& w.block_name(x, y, z) != "minecraft:powder_snow"
|
||||
}
|
||||
}
|
||||
impl Goal for Sleep {
|
||||
goal_clone!();
|
||||
fn flags(&self) -> u8 {
|
||||
MOVE | LOOK | JUMP
|
||||
}
|
||||
fn can_use(&mut self, m: &mut Mob, cx: &mut Cx) -> bool {
|
||||
m.ai.nav.is_done() && (self.can_sleep(m, cx.w) || flag(m, SLEEPING))
|
||||
}
|
||||
fn can_continue(&mut self, m: &mut Mob, cx: &mut Cx) -> bool {
|
||||
self.can_sleep(m, cx.w)
|
||||
}
|
||||
fn start(&mut self, m: &mut Mob, _: &mut Cx) {
|
||||
set_flag(m, SITTING | CROUCHING | INTERESTED, false);
|
||||
m.ai.jumping = false;
|
||||
set_flag(m, SLEEPING, true);
|
||||
m.nav_stop();
|
||||
let p = m.position;
|
||||
m.ai.mov.set_wanted(p.x, p.y, p.z, 0.0);
|
||||
}
|
||||
fn stop(&mut self, m: &mut Mob, _: &mut Cx) {
|
||||
self.countdown = m.random.next_int_bound(reduced(140)).unwrap_or(0);
|
||||
clear_states(m);
|
||||
}
|
||||
}
|
||||
|
||||
/// A shared goal under the fox's own conditions and start-up: the extra
|
||||
/// `canUse` of `FoxMeleeAttackGoal` and `FoxLookAtPlayerGoal` and the
|
||||
/// `clearStates` or `setIsInterested(false)` its subclasses do in `start`.
|
||||
#[derive(Clone, Debug)]
|
||||
struct Wrap<G> {
|
||||
goal: G,
|
||||
when: Option<fn(&Mob) -> bool>,
|
||||
/// The condition also applies to `canContinueToUse`.
|
||||
both: bool,
|
||||
on_start: Option<fn(&mut Mob)>,
|
||||
}
|
||||
impl<G: Goal + Clone + 'static> Wrap<G> {
|
||||
fn new(goal: G) -> Self {
|
||||
Self {
|
||||
goal,
|
||||
when: None,
|
||||
both: false,
|
||||
on_start: None,
|
||||
}
|
||||
}
|
||||
fn when(mut self, f: fn(&Mob) -> bool, both: bool) -> Self {
|
||||
(self.when, self.both) = (Some(f), both);
|
||||
self
|
||||
}
|
||||
fn on_start(mut self, f: fn(&mut Mob)) -> Self {
|
||||
self.on_start = Some(f);
|
||||
self
|
||||
}
|
||||
}
|
||||
impl<G: Goal + Clone + 'static> Goal for Wrap<G> {
|
||||
goal_clone!();
|
||||
fn flags(&self) -> u8 {
|
||||
self.goal.flags()
|
||||
}
|
||||
fn can_use(&mut self, m: &mut Mob, cx: &mut Cx) -> bool {
|
||||
self.when.is_none_or(|f| f(m)) && self.goal.can_use(m, cx)
|
||||
}
|
||||
fn can_continue(&mut self, m: &mut Mob, cx: &mut Cx) -> bool {
|
||||
(!self.both || self.when.is_none_or(|f| f(m))) && self.goal.can_continue(m, cx)
|
||||
}
|
||||
fn interruptable(&self) -> bool {
|
||||
self.goal.interruptable()
|
||||
}
|
||||
fn start(&mut self, m: &mut Mob, cx: &mut Cx) {
|
||||
if let Some(f) = self.on_start {
|
||||
f(m);
|
||||
}
|
||||
self.goal.start(m, cx);
|
||||
}
|
||||
fn stop(&mut self, m: &mut Mob, cx: &mut Cx) {
|
||||
self.goal.stop(m, cx);
|
||||
}
|
||||
fn every_tick(&self) -> bool {
|
||||
self.goal.every_tick()
|
||||
}
|
||||
fn tick(&mut self, m: &mut Mob, cx: &mut Cx) {
|
||||
self.goal.tick(m, cx);
|
||||
}
|
||||
fn name(&self) -> &'static str {
|
||||
self.goal.name()
|
||||
}
|
||||
}
|
||||
|
||||
/// `Fox.isPathClear`: six points along the way to the target, each with
|
||||
/// nothing but replaceable blocks in the three above it.
|
||||
fn path_clear(m: &Mob, w: &MobWorld, to: Vec3) -> bool {
|
||||
let (zdiff, xdiff) = (to.z - m.position.z, to.x - m.position.x);
|
||||
let slope = zdiff / xdiff;
|
||||
for i in 0..6 {
|
||||
let step = f64::from(i as f32 / 6.0);
|
||||
let z = if slope == 0.0 { 0.0 } else { zdiff * step };
|
||||
let x = if slope == 0.0 {
|
||||
xdiff * step
|
||||
} else {
|
||||
z / slope
|
||||
};
|
||||
for j in 1..4 {
|
||||
let at = [
|
||||
(m.position.x + x).floor() as i32,
|
||||
(m.position.y + f64::from(j)).floor() as i32,
|
||||
(m.position.z + z).floor() as i32,
|
||||
];
|
||||
if !w.facts.get(w.world.block(at[0], at[1], at[2])).replaceable {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
true
|
||||
}
|
||||
fn signum(x: f64) -> f64 {
|
||||
if x > 0.0 {
|
||||
1.0
|
||||
} else if x < 0.0 {
|
||||
-1.0
|
||||
} else {
|
||||
0.0
|
||||
}
|
||||
}
|
||||
|
||||
/// `Fox.FaceplantGoal`: stay put for 40 ticks with the nose in the snow.
|
||||
#[derive(Clone, Debug)]
|
||||
struct Faceplant {
|
||||
countdown: i32,
|
||||
}
|
||||
impl Goal for Faceplant {
|
||||
goal_clone!();
|
||||
fn flags(&self) -> u8 {
|
||||
MOVE | LOOK | JUMP
|
||||
}
|
||||
fn can_use(&mut self, m: &mut Mob, _: &mut Cx) -> bool {
|
||||
flag(m, FACEPLANTED)
|
||||
}
|
||||
fn can_continue(&mut self, m: &mut Mob, _: &mut Cx) -> bool {
|
||||
flag(m, FACEPLANTED) && self.countdown > 0
|
||||
}
|
||||
fn start(&mut self, _: &mut Mob, _: &mut Cx) {
|
||||
self.countdown = reduced(40);
|
||||
}
|
||||
fn stop(&mut self, m: &mut Mob, _: &mut Cx) {
|
||||
set_flag(m, FACEPLANTED, false);
|
||||
}
|
||||
fn tick(&mut self, _: &mut Mob, _: &mut Cx) {
|
||||
self.countdown -= 1;
|
||||
}
|
||||
}
|
||||
|
||||
/// `Fox.StalkPreyGoal`: creep up on a chicken or rabbit more than 6 blocks
|
||||
/// away, then crouch and keep still once it is within 6.
|
||||
#[derive(Clone, Debug)]
|
||||
struct StalkPrey;
|
||||
impl StalkPrey {
|
||||
fn look(m: &mut Mob, eye: [f64; 3]) {
|
||||
let (y, x) = (m.kind.max_head_y_rot, m.kind.max_head_x_rot);
|
||||
m.look_at_speed(eye, y, x);
|
||||
}
|
||||
}
|
||||
impl Goal for StalkPrey {
|
||||
goal_clone!();
|
||||
fn flags(&self) -> u8 {
|
||||
MOVE | LOOK
|
||||
}
|
||||
fn can_use(&mut self, m: &mut Mob, cx: &mut Cx) -> bool {
|
||||
if flag(m, SLEEPING) {
|
||||
return false;
|
||||
}
|
||||
let Some(t) = m.target_info(cx.w) else {
|
||||
return false;
|
||||
};
|
||||
let stalkable = match m.ai.target {
|
||||
Some(super::Who::Mob(id)) => {
|
||||
cx.w.seen(id)
|
||||
.is_some_and(|s| matches!(s.species, Species::Chicken | Species::Rabbit))
|
||||
}
|
||||
_ => false,
|
||||
};
|
||||
t.alive
|
||||
&& stalkable
|
||||
&& m.distance_sqr(t.position) > 36.0
|
||||
&& !flag(m, CROUCHING | INTERESTED)
|
||||
&& !m.ai.jumping
|
||||
}
|
||||
fn start(&mut self, m: &mut Mob, _: &mut Cx) {
|
||||
set_flag(m, SITTING | FACEPLANTED, false);
|
||||
}
|
||||
fn stop(&mut self, m: &mut Mob, cx: &mut Cx) {
|
||||
match m.target_info(cx.w) {
|
||||
Some(t) if path_clear(m, cx.w, t.position) => {
|
||||
set_flag(m, INTERESTED | CROUCHING, true);
|
||||
m.nav_stop();
|
||||
Self::look(m, [t.position.x, t.eye_y, t.position.z]);
|
||||
}
|
||||
_ => set_flag(m, INTERESTED | CROUCHING, false),
|
||||
}
|
||||
}
|
||||
fn tick(&mut self, m: &mut Mob, cx: &mut Cx) {
|
||||
let Some(t) = m.target_info(cx.w) else { return };
|
||||
Self::look(m, [t.position.x, t.eye_y, t.position.z]);
|
||||
if m.distance_sqr(t.position) <= 36.0 {
|
||||
set_flag(m, INTERESTED | CROUCHING, true);
|
||||
m.nav_stop();
|
||||
} else {
|
||||
let at = [t.position.x, t.position.y, t.position.z];
|
||||
m.nav_move_to_entity(cx.w, at, 0, 1.5);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// `Fox.FoxPounceGoal`: once fully crouched with a clear path, leap at the
|
||||
/// target, tilt with the flight and bite when within 2 blocks, or end up with
|
||||
/// the nose in snow.
|
||||
#[derive(Clone, Debug)]
|
||||
struct Pounce;
|
||||
impl Goal for Pounce {
|
||||
goal_clone!();
|
||||
fn flags(&self) -> u8 {
|
||||
MOVE | JUMP
|
||||
}
|
||||
fn can_use(&mut self, m: &mut Mob, cx: &mut Cx) -> bool {
|
||||
if !fully_crouched(m) {
|
||||
return false;
|
||||
}
|
||||
let Some(t) = m.target_info(cx.w).filter(|t| t.alive) else {
|
||||
return false;
|
||||
};
|
||||
let clear = path_clear(m, cx.w, t.position);
|
||||
if !clear {
|
||||
set_flag(m, CROUCHING | INTERESTED, false);
|
||||
}
|
||||
clear
|
||||
}
|
||||
fn can_continue(&mut self, m: &mut Mob, cx: &mut Cx) -> bool {
|
||||
if !m.target_info(cx.w).is_some_and(|t| t.alive) {
|
||||
return false;
|
||||
}
|
||||
let yd = m.velocity.y;
|
||||
(yd * yd >= f64::from(0.05f32) || m.head_pitch.abs() >= 15.0 || !m.on_ground)
|
||||
&& !flag(m, FACEPLANTED)
|
||||
}
|
||||
fn interruptable(&self) -> bool {
|
||||
false
|
||||
}
|
||||
fn start(&mut self, m: &mut Mob, cx: &mut Cx) {
|
||||
m.ai.jumping = true;
|
||||
set_flag(m, POUNCING, true);
|
||||
set_flag(m, INTERESTED, false);
|
||||
if let Some(t) = m.target_info(cx.w) {
|
||||
m.look_at_speed([t.position.x, t.eye_y, t.position.z], 60.0, 30.0);
|
||||
let u = m.position.vector_to(t.position).normalize();
|
||||
m.velocity = m.velocity.add_vector(Vec3::new(u.x * 0.8, 0.9, u.z * 0.8));
|
||||
}
|
||||
m.nav_stop();
|
||||
}
|
||||
fn stop(&mut self, m: &mut Mob, _: &mut Cx) {
|
||||
set_flag(m, CROUCHING | INTERESTED | POUNCING, false);
|
||||
m.anim.v[CROUCH_AMOUNT] = 0.0;
|
||||
m.anim.vo[CROUCH_AMOUNT] = 0.0;
|
||||
}
|
||||
fn tick(&mut self, m: &mut Mob, cx: &mut Cx) {
|
||||
let target = m.target_info(cx.w);
|
||||
if let Some(t) = &target {
|
||||
m.look_at_speed([t.position.x, t.eye_y, t.position.z], 60.0, 30.0);
|
||||
}
|
||||
if !flag(m, FACEPLANTED) {
|
||||
let v = m.velocity;
|
||||
if v.y * v.y < f64::from(0.03f32) && m.head_pitch != 0.0 {
|
||||
m.head_pitch += 0.2 * wrap(-m.head_pitch);
|
||||
} else {
|
||||
let horizontal = v.horizontal_distance();
|
||||
let bias = if m.ai.jumping && v.y > 0.0 { 6.5 } else { 1.0 };
|
||||
let y = v.y * bias;
|
||||
let len = (horizontal * horizontal + y * y).sqrt();
|
||||
if len > f64::from(1.0e-5f32) {
|
||||
let pitch = signum(-y) * jmath::acos(horizontal / len) * 180.0
|
||||
/ f64::from(std::f32::consts::PI);
|
||||
m.head_pitch = (pitch as f32 % 360.0).clamp(-90.0, 90.0);
|
||||
}
|
||||
}
|
||||
}
|
||||
if let (Some(t), Some(who)) = (&target, m.ai.target)
|
||||
&& m.distance_sqr(t.position) <= 4.0
|
||||
{
|
||||
super::ai::do_hurt_target(m, who, cx);
|
||||
} else if m.head_pitch > 0.0
|
||||
&& m.on_ground
|
||||
&& m.velocity.y as f32 != 0.0
|
||||
&& cx.w.block_name(
|
||||
m.position.x.floor() as i32,
|
||||
m.position.y.floor() as i32,
|
||||
m.position.z.floor() as i32,
|
||||
) == "minecraft:snow"
|
||||
{
|
||||
m.head_pitch = 60.0;
|
||||
m.ai.target = None;
|
||||
set_flag(m, FACEPLANTED, true);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// `Fox.SeekShelterGoal`: by day, or in a thunderstorm, leave open sky for
|
||||
/// somewhere the sky does not see. Villages are not told apart yet (vanilla
|
||||
/// stays put in one).
|
||||
#[derive(Clone, Debug)]
|
||||
struct SeekShelter {
|
||||
speed: f64,
|
||||
interval: i32,
|
||||
wanted: [f64; 3],
|
||||
}
|
||||
impl SeekShelter {
|
||||
fn new(speed: f64) -> Self {
|
||||
Self {
|
||||
speed,
|
||||
interval: reduced(100),
|
||||
wanted: [0.0; 3],
|
||||
}
|
||||
}
|
||||
fn set_wanted_pos(&mut self, m: &mut Mob, w: &MobWorld) -> bool {
|
||||
hide_pos(m, w).is_some_and(|at| {
|
||||
self.wanted = at;
|
||||
true
|
||||
})
|
||||
}
|
||||
}
|
||||
impl Goal for SeekShelter {
|
||||
goal_clone!();
|
||||
fn flags(&self) -> u8 {
|
||||
MOVE
|
||||
}
|
||||
fn can_use(&mut self, m: &mut Mob, cx: &mut Cx) -> bool {
|
||||
if flag(m, SLEEPING) || m.ai.target.is_some() {
|
||||
return false;
|
||||
}
|
||||
let at = [
|
||||
m.position.x.floor() as i32,
|
||||
m.position.y.floor() as i32,
|
||||
m.position.z.floor() as i32,
|
||||
];
|
||||
let sees_sky = cx.w.can_see_sky(at[0], at[1], at[2]);
|
||||
if cx.w.thundering && sees_sky {
|
||||
self.set_wanted_pos(m, cx.w)
|
||||
} else if self.interval > 0 {
|
||||
self.interval -= 1;
|
||||
false
|
||||
} else {
|
||||
self.interval = 100;
|
||||
cx.w.daylight && sees_sky && self.set_wanted_pos(m, cx.w)
|
||||
}
|
||||
}
|
||||
fn can_continue(&mut self, m: &mut Mob, _: &mut Cx) -> bool {
|
||||
!m.ai.nav.is_done()
|
||||
}
|
||||
fn start(&mut self, m: &mut Mob, cx: &mut Cx) {
|
||||
clear_states(m);
|
||||
m.nav_move_to(cx.w, self.wanted, self.speed);
|
||||
}
|
||||
}
|
||||
|
||||
/// `Fox.PerchAndSearchGoal`: now and then sit down and look around two to
|
||||
/// four times when nothing is going on.
|
||||
#[derive(Clone, Debug, Default)]
|
||||
struct Perch {
|
||||
rel: [f64; 2],
|
||||
look_time: i32,
|
||||
looks_remaining: i32,
|
||||
}
|
||||
impl Perch {
|
||||
fn reset_look(&mut self, m: &mut Mob) {
|
||||
let angle = std::f64::consts::TAU * m.random.next_double();
|
||||
self.rel = [angle.cos(), angle.sin()];
|
||||
self.look_time = reduced(80 + m.random.next_int_bound(20).unwrap_or(0));
|
||||
}
|
||||
}
|
||||
impl Goal for Perch {
|
||||
goal_clone!();
|
||||
fn flags(&self) -> u8 {
|
||||
MOVE | LOOK
|
||||
}
|
||||
fn can_use(&mut self, m: &mut Mob, cx: &mut Cx) -> bool {
|
||||
m.ai.last_hurt_by.is_none()
|
||||
&& m.random.next_float() < 0.02
|
||||
&& !flag(m, SLEEPING)
|
||||
&& m.ai.target.is_none()
|
||||
&& m.ai.nav.is_done()
|
||||
&& !alertable(m, cx.w)
|
||||
&& !flag(m, POUNCING | CROUCHING)
|
||||
}
|
||||
fn can_continue(&mut self, _: &mut Mob, _: &mut Cx) -> bool {
|
||||
self.looks_remaining > 0
|
||||
}
|
||||
fn start(&mut self, m: &mut Mob, _: &mut Cx) {
|
||||
self.reset_look(m);
|
||||
self.looks_remaining = 2 + m.random.next_int_bound(3).unwrap_or(0);
|
||||
set_flag(m, SITTING, true);
|
||||
m.nav_stop();
|
||||
}
|
||||
fn stop(&mut self, m: &mut Mob, _: &mut Cx) {
|
||||
set_flag(m, SITTING, false);
|
||||
}
|
||||
fn tick(&mut self, m: &mut Mob, _: &mut Cx) {
|
||||
self.look_time -= 1;
|
||||
if self.look_time <= 0 {
|
||||
self.looks_remaining -= 1;
|
||||
self.reset_look(m);
|
||||
}
|
||||
let at = [
|
||||
m.position.x + self.rel[0],
|
||||
m.eye_y(),
|
||||
m.position.z + self.rel[1],
|
||||
];
|
||||
let (y, x) = (m.kind.max_head_y_rot, m.kind.max_head_x_rot);
|
||||
m.look_at_speed(at, y, x);
|
||||
}
|
||||
}
|
||||
|
||||
/// `BlockState` with one property changed.
|
||||
fn with_prop(w: &MobWorld, p: [i32; 3], key: &str, value: &str) -> Option<BlockStateId> {
|
||||
let mut s = w.registry.state(w.world.block(p[0], p[1], p[2]))?.clone();
|
||||
s.properties.insert(key.into(), value.into());
|
||||
w.registry.id(&s)
|
||||
}
|
||||
/// `FoxEatBerriesGoal.isValidTarget`: a ripe sweet berry bush or glow berries.
|
||||
fn has_berries(w: &MobWorld, p: [i32; 3]) -> bool {
|
||||
match w.block_name(p[0], p[1], p[2]) {
|
||||
"minecraft:sweet_berry_bush" => w
|
||||
.prop(p[0], p[1], p[2], "age")
|
||||
.and_then(|a| a.parse::<i32>().ok())
|
||||
.is_some_and(|a| a >= 2),
|
||||
"minecraft:cave_vines" | "minecraft:cave_vines_plant" => {
|
||||
w.prop(p[0], p[1], p[2], "berries") == Some("true")
|
||||
}
|
||||
_ => false,
|
||||
}
|
||||
}
|
||||
|
||||
/// `Fox.FoxEatBerriesGoal`: walk to a ripe berry bush and pick it after 40 ticks.
|
||||
#[derive(Clone, Debug)]
|
||||
struct EatBerries {
|
||||
block: MoveToBlock,
|
||||
waited: i32,
|
||||
}
|
||||
impl EatBerries {
|
||||
fn new() -> Self {
|
||||
Self {
|
||||
block: MoveToBlock::new(1.2, 12, 1),
|
||||
waited: 0,
|
||||
}
|
||||
}
|
||||
fn pick(&mut self, m: &mut Mob, cx: &mut Cx) {
|
||||
let p = self.block.block;
|
||||
let at = [
|
||||
f64::from(p[0]) + 0.5,
|
||||
f64::from(p[1]) + 0.5,
|
||||
f64::from(p[2]) + 0.5,
|
||||
];
|
||||
if cx.w.block_name(p[0], p[1], p[2]) == "minecraft:sweet_berry_bush" {
|
||||
let age: i32 =
|
||||
cx.w.prop(p[0], p[1], p[2], "age")
|
||||
.and_then(|a| a.parse().ok())
|
||||
.unwrap_or(0);
|
||||
let mut count = 1 + m.random.next_int_bound(2).unwrap_or(0) + i32::from(age == 3);
|
||||
if m.extra.main_hand.is_none() {
|
||||
m.extra.main_hand = Some("minecraft:sweet_berries".into());
|
||||
count -= 1;
|
||||
}
|
||||
if count > 0 {
|
||||
cx.events.push(MobEvent::DropItem {
|
||||
item: "minecraft:sweet_berries".into(),
|
||||
count: count as u32,
|
||||
at,
|
||||
});
|
||||
}
|
||||
if let Some(state) = with_prop(cx.w, p, "age", "1") {
|
||||
cx.events.push(MobEvent::SetBlock { pos: p, state });
|
||||
}
|
||||
cx.events.push(MobEvent::Sound(
|
||||
"block.sweet_berry_bush.pick_berries",
|
||||
at,
|
||||
1.0,
|
||||
1.0,
|
||||
));
|
||||
} else {
|
||||
// CaveVines.use: one glow berry, the vine goes back to bare.
|
||||
cx.events.push(MobEvent::DropItem {
|
||||
item: "minecraft:glow_berries".into(),
|
||||
count: 1,
|
||||
at,
|
||||
});
|
||||
if let Some(state) = with_prop(cx.w, p, "berries", "false") {
|
||||
cx.events.push(MobEvent::SetBlock { pos: p, state });
|
||||
}
|
||||
let pitch = 0.8 + m.random.next_float() * 0.4;
|
||||
cx.events.push(MobEvent::Sound(
|
||||
"block.cave_vines.pick_berries",
|
||||
at,
|
||||
1.0,
|
||||
pitch,
|
||||
));
|
||||
}
|
||||
}
|
||||
}
|
||||
impl Goal for EatBerries {
|
||||
goal_clone!();
|
||||
fn flags(&self) -> u8 {
|
||||
MOVE | JUMP
|
||||
}
|
||||
fn can_use(&mut self, m: &mut Mob, cx: &mut Cx) -> bool {
|
||||
if flag(m, SLEEPING) {
|
||||
return false;
|
||||
}
|
||||
// MoveToBlockGoal.canUse draws the next delay only once the last one
|
||||
// has run out.
|
||||
if self.block.next_start > 0 {
|
||||
self.block.next_start -= 1;
|
||||
return false;
|
||||
}
|
||||
let next = reduced(200 + m.random.next_int_bound(200).unwrap_or(0));
|
||||
self.block.can_use(m, cx.w, next, has_berries)
|
||||
}
|
||||
fn can_continue(&mut self, _: &mut Mob, cx: &mut Cx) -> bool {
|
||||
self.block.can_continue(cx.w, has_berries)
|
||||
}
|
||||
fn start(&mut self, m: &mut Mob, cx: &mut Cx) {
|
||||
self.waited = 0;
|
||||
set_flag(m, SITTING, false);
|
||||
self.block.start(m, cx.w);
|
||||
}
|
||||
fn every_tick(&self) -> bool {
|
||||
true
|
||||
}
|
||||
fn tick(&mut self, m: &mut Mob, cx: &mut Cx) {
|
||||
if self.block.reached {
|
||||
if self.waited >= 40 {
|
||||
if cx.w.mob_griefing {
|
||||
self.pick(m, cx);
|
||||
}
|
||||
} else {
|
||||
self.waited += 1;
|
||||
}
|
||||
} else if m.random.next_float() < 0.05 {
|
||||
let at = [m.position.x, m.position.y, m.position.z];
|
||||
cx.events
|
||||
.push(MobEvent::Sound("entity.fox.sniff", at, 1.0, 1.0));
|
||||
}
|
||||
self.block.tick_within(m, cx.w, 2.0);
|
||||
}
|
||||
}
|
||||
|
||||
/// `FoxRenderState` as the renderer reads it: the flags, `getHeadRollAngle`
|
||||
/// and `getCrouchAmount` at the partial tick. `FoxModel.legMotionPos` gains
|
||||
/// 0.67 a frame; this takes 60 frames a second.
|
||||
pub fn pose(m: &Mob, partial: f32) -> super::body_anim::Fox {
|
||||
super::body_anim::Fox {
|
||||
crouching: flag(m, CROUCHING),
|
||||
sleeping: flag(m, SLEEPING),
|
||||
sitting: flag(m, SITTING),
|
||||
pouncing: flag(m, POUNCING),
|
||||
faceplanted: flag(m, FACEPLANTED),
|
||||
head_roll_angle: m.anim.lerp(INTERESTED_ANGLE, partial) * 0.11 * std::f32::consts::PI,
|
||||
crouch_amount: m.anim.lerp(CROUCH_AMOUNT, partial),
|
||||
leg_motion_pos: (m.age as f32 + partial) * 0.67 * 3.0,
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,301 @@
|
||||
//! Frogs (`Frog`, `FrogAi`): the `Croak` and `ShootTongue` behaviours as
|
||||
//! goals, the poses they set and the client's animation states.
|
||||
//! AnimState states: jump, croak, tongue, swim idle.
|
||||
//!
|
||||
//! The brain's idle `RunOne` picks a croak three times as often as a stroll
|
||||
//! (weights 3 and 1; it also holds a do-nothing option that only repeats
|
||||
//! the pick), so a frog that is not walking croaks in three of four idle
|
||||
//! picks: `Croak` takes those, and the frog's stroll then follows at once.
|
||||
use super::{
|
||||
Mob, MobEvent, MobWorld, Species, Who,
|
||||
goals::{
|
||||
Cx, Float, Goal, LOOK, LookAtPlayer, MOVE, Panic, RandomLookAround, Stroll, goal_clone,
|
||||
},
|
||||
sync,
|
||||
};
|
||||
use horizon_core::{geometry::Vec3, random::RandomSource};
|
||||
|
||||
const JUMP: usize = 0;
|
||||
const CROAK: usize = 1;
|
||||
const TONGUE: usize = 2;
|
||||
const SWIM_IDLE: usize = 3;
|
||||
|
||||
/// `Croak.CROAK_TICKS`.
|
||||
const CROAK_TICKS: i32 = 60;
|
||||
/// `ShootTongue`'s duration and eating distance.
|
||||
const TONGUE_TIME_OUT: i32 = 100;
|
||||
const EATING_DISTANCE: f64 = 1.75;
|
||||
/// `FrogAttackablesSensor.TARGET_DETECTION_DISTANCE` and the sensor's scan
|
||||
/// rate.
|
||||
const DETECTION_DISTANCE: f64 = 10.0;
|
||||
const SCAN_RATE: i32 = 20;
|
||||
|
||||
/// `ShootTongue.State`.
|
||||
#[derive(Clone, Copy, Debug, Default, PartialEq)]
|
||||
enum Phase {
|
||||
MoveToTarget,
|
||||
CatchAnimation,
|
||||
EatAnimation,
|
||||
#[default]
|
||||
Done,
|
||||
}
|
||||
|
||||
/// The frog's brain memories and `DATA_TONGUE_TARGET_ID`.
|
||||
#[derive(Clone, Debug, Default)]
|
||||
pub struct State {
|
||||
croak: i32,
|
||||
/// Ticks to the attackables sensor's next scan, and `NEAREST_ATTACKABLE`.
|
||||
scan: i32,
|
||||
nearest: Option<u32>,
|
||||
/// `UNREACHABLE_TONGUE_TARGETS`: the mob and the ticks it stays in mind.
|
||||
unreachable: Vec<(u32, i32)>,
|
||||
/// `ATTACK_TARGET` while `ShootTongue` runs, and the synched tongue target.
|
||||
target: Option<u32>,
|
||||
pub tongue_target: Option<u32>,
|
||||
phase: Phase,
|
||||
eat_timer: i32,
|
||||
path_counter: i32,
|
||||
time: i32,
|
||||
}
|
||||
|
||||
pub fn register(m: &mut Mob) {
|
||||
let g = &mut m.ai.goals;
|
||||
g.add(0, Float);
|
||||
g.add(1, Panic::new(1.25));
|
||||
g.add(2, ShootTongue);
|
||||
g.add(4, Croak);
|
||||
g.add(5, Stroll::avoiding_water(1.0).interval(1));
|
||||
g.add(6, LookAtPlayer::new(6.0));
|
||||
g.add(7, RandomLookAround::default());
|
||||
}
|
||||
|
||||
/// `Entity.setPose` and `Frog.onSyncedDataUpdated(DATA_POSE)`.
|
||||
pub fn set_pose(m: &mut Mob, id: u8) {
|
||||
let old = std::mem::replace(&mut m.extra.pose, id);
|
||||
if old != id {
|
||||
pose_changed(m);
|
||||
}
|
||||
}
|
||||
/// The animation states of the pose the frog has taken.
|
||||
pub fn pose_changed(m: &mut Mob) {
|
||||
let age = m.age as f32;
|
||||
let pose = m.extra.pose;
|
||||
let states = &mut m.anim.states;
|
||||
for (state, on) in [
|
||||
(JUMP, pose == sync::LONG_JUMPING),
|
||||
(CROAK, pose == sync::CROAKING),
|
||||
(TONGUE, pose == sync::USING_TONGUE),
|
||||
] {
|
||||
if on {
|
||||
states[state].start(age);
|
||||
} else {
|
||||
states[state].stop();
|
||||
}
|
||||
}
|
||||
}
|
||||
/// `Frog.tick` on a client: the idle swim while it floats still. The server
|
||||
/// runs it too, for a window that shows its own mobs.
|
||||
pub fn tick(m: &mut Mob) {
|
||||
let age = m.age as f32;
|
||||
let still = m.fluids.in_water && m.limb_speed <= 1.0e-5;
|
||||
m.anim.states[SWIM_IDLE].animate_when(still, age);
|
||||
}
|
||||
|
||||
/// `Frog.canEat`: a slime or magma cube of size 1.
|
||||
fn can_eat(species: Species, size: i32) -> bool {
|
||||
matches!(species, Species::Slime | Species::MagmaCube) && size == 1
|
||||
}
|
||||
|
||||
/// `FrogAttackablesSensor` (every 20 ticks) and the memory timers.
|
||||
pub fn server_ai_step(m: &mut Mob, w: &MobWorld) {
|
||||
let f = &mut m.extra.frog;
|
||||
f.unreachable.retain_mut(|(_, ttl)| {
|
||||
*ttl -= 1;
|
||||
*ttl > 0
|
||||
});
|
||||
f.scan -= 1;
|
||||
if f.scan > 0 {
|
||||
return;
|
||||
}
|
||||
f.scan = SCAN_RATE;
|
||||
let unreachable: Vec<u32> = f.unreachable.iter().map(|u| u.0).collect();
|
||||
let eye = Vec3::new(m.position.x, m.eye_y(), m.position.z);
|
||||
let nearest = w
|
||||
.nearby(m.position, DETECTION_DISTANCE, |s| {
|
||||
can_eat(s.species, s.size)
|
||||
&& !unreachable.contains(&s.id)
|
||||
&& w.line_of_sight(eye, Vec3::new(s.position.x, s.eye_y, s.position.z))
|
||||
})
|
||||
.find(|s| s.position.distance_squared(m.position) < DETECTION_DISTANCE * DETECTION_DISTANCE)
|
||||
.map(|s| s.id);
|
||||
m.extra.frog.nearest = nearest;
|
||||
}
|
||||
|
||||
/// `Croak`: the pose sync::CROAKING for 60 ticks (the croak is the frog's ambient
|
||||
/// sound).
|
||||
#[derive(Clone, Debug)]
|
||||
struct Croak;
|
||||
impl Goal for Croak {
|
||||
goal_clone!();
|
||||
fn flags(&self) -> u8 {
|
||||
MOVE
|
||||
}
|
||||
fn can_use(&mut self, m: &mut Mob, _cx: &mut Cx) -> bool {
|
||||
// checkExtraStartConditions (standing), no walk target, and not in
|
||||
// a liquid (start does nothing there).
|
||||
m.extra.pose == sync::STANDING
|
||||
&& m.on_ground
|
||||
&& !m.fluids.in_water
|
||||
&& !m.fluids.in_lava
|
||||
&& m.ai.nav.is_done()
|
||||
&& m.random.next_int_bound(4).unwrap_or(0) != 0
|
||||
}
|
||||
fn can_continue(&mut self, m: &mut Mob, _cx: &mut Cx) -> bool {
|
||||
m.extra.frog.croak < CROAK_TICKS
|
||||
}
|
||||
fn start(&mut self, m: &mut Mob, _cx: &mut Cx) {
|
||||
m.extra.frog.croak = 0;
|
||||
set_pose(m, sync::CROAKING);
|
||||
}
|
||||
fn stop(&mut self, m: &mut Mob, _cx: &mut Cx) {
|
||||
set_pose(m, sync::STANDING);
|
||||
}
|
||||
fn tick(&mut self, m: &mut Mob, _cx: &mut Cx) {
|
||||
m.extra.frog.croak += 1;
|
||||
}
|
||||
fn every_tick(&self) -> bool {
|
||||
true
|
||||
}
|
||||
}
|
||||
|
||||
/// `ShootTongue` with the tongue activity's `StartAttacking` and
|
||||
/// `StopAttackingIfTargetInvalid`: walk up to a small slime or magma cube,
|
||||
/// shoot the tongue, pull it in and eat it.
|
||||
#[derive(Clone, Debug)]
|
||||
struct ShootTongue;
|
||||
impl Goal for ShootTongue {
|
||||
goal_clone!();
|
||||
fn flags(&self) -> u8 {
|
||||
MOVE | LOOK
|
||||
}
|
||||
fn can_use(&mut self, m: &mut Mob, cx: &mut Cx) -> bool {
|
||||
let Some(id) = m.extra.frog.nearest else {
|
||||
return false;
|
||||
};
|
||||
// The start conditions: no walk target, no panic, not croaking.
|
||||
if m.ai.panicking || m.extra.pose == sync::CROAKING || !m.ai.nav.is_done() {
|
||||
return false;
|
||||
}
|
||||
let Some(t) = cx.w.seen(id).filter(|t| t.alive) else {
|
||||
m.extra.frog.nearest = None;
|
||||
return false;
|
||||
};
|
||||
// canPathfindToTarget: a path that ends at the target.
|
||||
let at = [t.position.x, t.position.y, t.position.z];
|
||||
let ends = m
|
||||
.create_path_to_entity(cx.w, at, 0)
|
||||
.is_some_and(|p| f64::from(p.dist_to_target) < EATING_DISTANCE);
|
||||
if !ends {
|
||||
// The target is forgotten and not taken for 100 ticks.
|
||||
let f = &mut m.extra.frog;
|
||||
f.nearest = None;
|
||||
if !f.unreachable.iter().any(|u| u.0 == id) {
|
||||
if f.unreachable.len() == 5 {
|
||||
f.unreachable.remove(0);
|
||||
}
|
||||
f.unreachable.push((id, 100));
|
||||
}
|
||||
return false;
|
||||
}
|
||||
m.extra.frog.target = Some(id);
|
||||
true
|
||||
}
|
||||
fn can_continue(&mut self, m: &mut Mob, cx: &mut Cx) -> bool {
|
||||
let f = &m.extra.frog;
|
||||
f.phase != Phase::Done
|
||||
&& f.time <= TONGUE_TIME_OUT
|
||||
&& !m.ai.panicking
|
||||
&& f.target
|
||||
.is_some_and(|id| cx.w.seen(id).is_some_and(|t| t.alive))
|
||||
}
|
||||
fn start(&mut self, m: &mut Mob, cx: &mut Cx) {
|
||||
let Some(t) = m.extra.frog.target.and_then(|id| cx.w.seen(id)) else {
|
||||
return;
|
||||
};
|
||||
let at = [t.position.x, t.position.y, t.position.z];
|
||||
let eye = [t.position.x, t.eye_y, t.position.z];
|
||||
m.look_at(eye);
|
||||
m.extra.frog.tongue_target = m.extra.frog.target;
|
||||
m.nav_move_to_entity(cx.w, at, 0, 2.0);
|
||||
let f = &mut m.extra.frog;
|
||||
f.path_counter = 10;
|
||||
f.time = 0;
|
||||
f.phase = Phase::MoveToTarget;
|
||||
}
|
||||
fn stop(&mut self, m: &mut Mob, _cx: &mut Cx) {
|
||||
let f = &mut m.extra.frog;
|
||||
f.target = None;
|
||||
f.tongue_target = None;
|
||||
f.phase = Phase::Done;
|
||||
set_pose(m, sync::STANDING);
|
||||
}
|
||||
fn every_tick(&self) -> bool {
|
||||
true
|
||||
}
|
||||
fn tick(&mut self, m: &mut Mob, cx: &mut Cx) {
|
||||
let Some(id) = m.extra.frog.target else {
|
||||
return;
|
||||
};
|
||||
let Some(t) = cx.w.seen(id) else {
|
||||
return;
|
||||
};
|
||||
let (at, pos) = ([t.position.x, t.position.y, t.position.z], t.position);
|
||||
m.look_at([t.position.x, t.eye_y, t.position.z]);
|
||||
m.extra.frog.tongue_target = Some(id);
|
||||
m.extra.frog.time += 1;
|
||||
let here = [m.position.x, m.position.y, m.position.z];
|
||||
match m.extra.frog.phase {
|
||||
Phase::MoveToTarget => {
|
||||
if pos.distance_squared(m.position) < EATING_DISTANCE * EATING_DISTANCE {
|
||||
cx.events
|
||||
.push(MobEvent::Sound("entity.frog.tongue", here, 2.0, 1.0));
|
||||
set_pose(m, sync::USING_TONGUE);
|
||||
// The target is drawn in at 0.75 a tick.
|
||||
let pull = m.position.subtract(pos).normalize().scale(0.75);
|
||||
cx.events.push(MobEvent::Push {
|
||||
target: Who::Mob(id),
|
||||
velocity: [pull.x, pull.y, pull.z],
|
||||
});
|
||||
let f = &mut m.extra.frog;
|
||||
f.eat_timer = 0;
|
||||
f.phase = Phase::CatchAnimation;
|
||||
} else if m.extra.frog.path_counter <= 0 {
|
||||
m.nav_move_to_entity(cx.w, at, 0, 2.0);
|
||||
m.extra.frog.path_counter = 10;
|
||||
} else {
|
||||
m.extra.frog.path_counter -= 1;
|
||||
}
|
||||
}
|
||||
Phase::CatchAnimation => {
|
||||
let f = &mut m.extra.frog;
|
||||
f.eat_timer += 1;
|
||||
if f.eat_timer > 6 {
|
||||
f.phase = Phase::EatAnimation;
|
||||
// eatEntity.
|
||||
cx.events
|
||||
.push(MobEvent::Sound("entity.frog.eat", here, 2.0, 1.0));
|
||||
super::ai::do_hurt_target(m, Who::Mob(id), cx);
|
||||
}
|
||||
}
|
||||
Phase::EatAnimation => {
|
||||
let f = &mut m.extra.frog;
|
||||
if f.eat_timer >= 10 {
|
||||
f.phase = Phase::Done;
|
||||
} else {
|
||||
f.eat_timer += 1;
|
||||
}
|
||||
}
|
||||
Phase::Done => {}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -654,12 +654,15 @@ impl Goal for Float {
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct Panic {
|
||||
pub speed: f64,
|
||||
/// Only the damage of `#panic_environmental_causes` (an armadillo).
|
||||
pub environmental: bool,
|
||||
pos: [f64; 3],
|
||||
}
|
||||
impl Panic {
|
||||
pub fn new(speed: f64) -> Self {
|
||||
Self {
|
||||
speed,
|
||||
environmental: false,
|
||||
pos: [0.0; 3],
|
||||
}
|
||||
}
|
||||
@@ -675,7 +678,14 @@ impl Goal for Panic {
|
||||
MOVE
|
||||
}
|
||||
fn can_use(&mut self, m: &mut Mob, cx: &mut Cx) -> bool {
|
||||
if !should_panic(m) {
|
||||
let wanted = if self.environmental {
|
||||
m.last_damage.is_some_and(|(d, t)| {
|
||||
super::armadillo::environmental(d.kind) && i64::from(m.age) - t <= 40
|
||||
}) || m.fire_ticks > 0 && !m.kind.flags.fire_immune
|
||||
} else {
|
||||
should_panic(m)
|
||||
};
|
||||
if !wanted {
|
||||
return false;
|
||||
}
|
||||
if m.fire_ticks > 0 {
|
||||
@@ -1442,6 +1452,9 @@ pub struct HurtByTarget {
|
||||
/// `Bee.BeeHurtByOtherGoal`: kept only while angry, and alerting others
|
||||
/// only when the attacker is in sight.
|
||||
pub bee: bool,
|
||||
/// Whom `alertOther` calls: a filter on the mob (`PandaHurtByTargetGoal`
|
||||
/// calls aggressive pandas only).
|
||||
pub alert_if: Option<fn(&Seen) -> bool>,
|
||||
/// `PolarBear.PolarBearHurtByTargetGoal`: a hurt cub only calls the
|
||||
/// adult bears and drops the target itself.
|
||||
pub polar_bear: bool,
|
||||
@@ -1455,6 +1468,7 @@ impl HurtByTarget {
|
||||
alert_except: vec![],
|
||||
ignore: vec![],
|
||||
bee: false,
|
||||
alert_if: None,
|
||||
polar_bear: false,
|
||||
timestamp: 0,
|
||||
st: TargetState {
|
||||
@@ -1475,6 +1489,10 @@ impl HurtByTarget {
|
||||
self.bee = true;
|
||||
self
|
||||
}
|
||||
pub fn alert_if(mut self, f: fn(&Seen) -> bool) -> Self {
|
||||
self.alert_if = Some(f);
|
||||
self
|
||||
}
|
||||
pub fn polar_bear(mut self) -> Self {
|
||||
self.polar_bear = true;
|
||||
self
|
||||
@@ -1552,6 +1570,7 @@ impl Goal for HurtByTarget {
|
||||
&& (s.position.z - pos.z).abs() <= range + 1.0
|
||||
})
|
||||
.filter(|s| !self.alert_except.contains(&s.species))
|
||||
.filter(|s| self.alert_if.is_none_or(|f| f(s)))
|
||||
// PolarBearHurtByTargetGoal.alertOther: adults only.
|
||||
.filter(|s| !(self.polar_bear && s.baby))
|
||||
.filter(|s| Who::Mob(s.id) != by)
|
||||
@@ -1591,6 +1610,10 @@ pub struct AvoidEntity {
|
||||
pub sprint: f64,
|
||||
/// Extra condition on the avoider (e.g. untamed cats only).
|
||||
pub when: Option<fn(&Mob) -> bool>,
|
||||
/// Extra condition on the player or mob avoided (`AvoidEntityGoal`'s
|
||||
/// predicate: a sneaking player, a tame wolf).
|
||||
pub player_when: Option<fn(&super::PlayerView) -> bool>,
|
||||
pub seen_when: Option<fn(&Seen) -> bool>,
|
||||
from: Vec3,
|
||||
/// The mob being avoided (`toAvoid`), when it is one.
|
||||
pub found: Option<u32>,
|
||||
@@ -1603,6 +1626,8 @@ impl AvoidEntity {
|
||||
walk,
|
||||
sprint,
|
||||
when: None,
|
||||
player_when: None,
|
||||
seen_when: None,
|
||||
from: Vec3::ZERO,
|
||||
found: None,
|
||||
}
|
||||
@@ -1611,6 +1636,14 @@ impl AvoidEntity {
|
||||
self.when = Some(f);
|
||||
self
|
||||
}
|
||||
pub fn player_when(mut self, f: fn(&super::PlayerView) -> bool) -> Self {
|
||||
self.player_when = Some(f);
|
||||
self
|
||||
}
|
||||
pub fn seen_when(mut self, f: fn(&Seen) -> bool) -> Self {
|
||||
self.seen_when = Some(f);
|
||||
self
|
||||
}
|
||||
}
|
||||
impl Goal for AvoidEntity {
|
||||
goal_clone!();
|
||||
@@ -1631,6 +1664,7 @@ impl Goal for AvoidEntity {
|
||||
.filter(|p| {
|
||||
p.alive
|
||||
&& !p.creative
|
||||
&& self.player_when.is_none_or(|f| f(p))
|
||||
&& m.distance_sqr(p.position) <= max * max
|
||||
&& (p.position.y - m.position.y).abs() <= max + 3.0
|
||||
})
|
||||
@@ -1639,10 +1673,12 @@ impl Goal for AvoidEntity {
|
||||
Avoid::Species(list) => {
|
||||
let id = m.id;
|
||||
let y = m.position.y;
|
||||
let seen_when = self.seen_when;
|
||||
let hit =
|
||||
cx.w.nearby(m.position, max, move |s| {
|
||||
s.id != id
|
||||
&& list.contains(&s.species)
|
||||
&& seen_when.is_none_or(|f| f(s))
|
||||
&& (s.position.y - y).abs() <= 3.0 + max
|
||||
})
|
||||
.map(|s| (s.id, s.position))
|
||||
@@ -1778,7 +1814,7 @@ impl Goal for Tempt {
|
||||
}
|
||||
fn tick(&mut self, m: &mut Mob, cx: &mut Cx) {
|
||||
let Some(p) = cx.w.player.clone() else { return };
|
||||
let (y, x) = (m.kind.max_head_y_rot + 20.0, m.kind.max_head_x_rot);
|
||||
let (y, x) = (m.kind.max_head_y_rot + 20.0, m.max_head_x_rot());
|
||||
m.look_at_speed(p.eye, y, x);
|
||||
if m.distance_sqr(p.position) < self.stop_distance * self.stop_distance {
|
||||
m.nav_stop();
|
||||
@@ -1887,7 +1923,7 @@ impl Goal for Breed {
|
||||
let Some(p) = self.partner.and_then(|p| cx.w.seen(p)).cloned() else {
|
||||
return;
|
||||
};
|
||||
let x = m.kind.max_head_x_rot;
|
||||
let x = m.max_head_x_rot();
|
||||
m.look_at_speed([p.position.x, p.eye_y, p.position.z], 10.0, x);
|
||||
m.nav_move_to_entity(
|
||||
cx.w,
|
||||
@@ -2021,6 +2057,30 @@ impl Goal for RestrictSun {
|
||||
m.ai.nav.avoid_sun = false;
|
||||
}
|
||||
}
|
||||
/// `FleeSunGoal.getHidePos`: a spot within 10 blocks that the sky does not
|
||||
/// see and the mob likes.
|
||||
pub fn hide_pos(m: &mut Mob, w: &MobWorld) -> Option<[f64; 3]> {
|
||||
let (bx, by, bz) = (
|
||||
m.position.x.floor() as i32,
|
||||
m.position.y.floor() as i32,
|
||||
m.position.z.floor() as i32,
|
||||
);
|
||||
for _ in 0..10 {
|
||||
let p = [
|
||||
bx + m.random.next_int_bound(20).unwrap_or(0) - 10,
|
||||
by + m.random.next_int_bound(6).unwrap_or(0) - 3,
|
||||
bz + m.random.next_int_bound(20).unwrap_or(0) - 10,
|
||||
];
|
||||
if !w.can_see_sky(p[0], p[1], p[2]) && m.walk_target_value(w, p) < 0.0 {
|
||||
return Some([
|
||||
f64::from(p[0]) + 0.5,
|
||||
f64::from(p[1]),
|
||||
f64::from(p[2]) + 0.5,
|
||||
]);
|
||||
}
|
||||
}
|
||||
None
|
||||
}
|
||||
/// FleeSunGoal: find shade while burning.
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct FleeSun {
|
||||
@@ -2052,22 +2112,13 @@ impl Goal for FleeSun {
|
||||
if !cx.w.can_see_sky(bx, by, bz) {
|
||||
return false;
|
||||
}
|
||||
for _ in 0..10 {
|
||||
let p = [
|
||||
bx + m.random.next_int_bound(20).unwrap_or(0) - 10,
|
||||
by + m.random.next_int_bound(6).unwrap_or(0) - 3,
|
||||
bz + m.random.next_int_bound(20).unwrap_or(0) - 10,
|
||||
];
|
||||
if !cx.w.can_see_sky(p[0], p[1], p[2]) && m.walk_target_value(cx.w, p) < 0.0 {
|
||||
self.wanted = [
|
||||
f64::from(p[0]) + 0.5,
|
||||
f64::from(p[1]),
|
||||
f64::from(p[2]) + 0.5,
|
||||
];
|
||||
return true;
|
||||
match hide_pos(m, cx.w) {
|
||||
Some(at) => {
|
||||
self.wanted = at;
|
||||
true
|
||||
}
|
||||
None => false,
|
||||
}
|
||||
false
|
||||
}
|
||||
fn can_continue(&mut self, m: &mut Mob, _: &mut Cx) -> bool {
|
||||
!m.ai.nav.is_done()
|
||||
|
||||
@@ -0,0 +1,443 @@
|
||||
//! Goats (`Goat`, `GoatAi`): the lowered head (`lowerHeadTick`, entity
|
||||
//! events 58 and 59) and the ram activity: `PrepareRamNearestTarget` picks
|
||||
//! the nearest living thing it can see, walks to a spot four to seven
|
||||
//! blocks away in a straight line and waits there 20 ticks with its head
|
||||
//! down; `RamTarget` then charges at it, knocks it back, or breaks a horn
|
||||
//! on a log, stone or ore. Both are one goal here.
|
||||
use super::{
|
||||
Damage, Mob, MobEvent, MobWorld, Who,
|
||||
goals::{Cx, Goal, LOOK, MOVE, goal_clone},
|
||||
};
|
||||
use horizon_core::{geometry::Vec3, random::RandomSource};
|
||||
|
||||
/// `GoatAi.TIME_BETWEEN_RAMS` and `TIME_BETWEEN_RAMS_SCREAMER`.
|
||||
const RAMS: (i32, i32) = (600, 6000);
|
||||
const RAMS_SCREAMER: (i32, i32) = (100, 300);
|
||||
const RAM_PREPARE_TIME: i64 = 20;
|
||||
const MIN_RAM_DISTANCE: i32 = 4;
|
||||
const MAX_RAM_DISTANCE: i32 = 7;
|
||||
/// `PrepareRamNearestTarget.TIME_OUT_DURATION` and `RamTarget`'s.
|
||||
const PREPARE_TIME_OUT: i32 = 160;
|
||||
const RAM_TIME_OUT: i32 = 200;
|
||||
/// `NEAREST_VISIBLE_LIVING_ENTITIES` reaches 16 blocks.
|
||||
const SENSE_RANGE: f64 = 16.0;
|
||||
|
||||
#[derive(Clone, Copy, Debug, Default, PartialEq)]
|
||||
enum Phase {
|
||||
#[default]
|
||||
Done,
|
||||
Prepare,
|
||||
/// `RAM_TARGET` is set; `RamTarget` starts on the next tick.
|
||||
RamStart,
|
||||
Ram,
|
||||
}
|
||||
#[derive(Clone, Copy, Debug)]
|
||||
struct Candidate {
|
||||
start: [i32; 3],
|
||||
target_pos: [i32; 3],
|
||||
target: Who,
|
||||
}
|
||||
|
||||
/// `RAM_COOLDOWN_TICKS`, the lowered head and the ram behaviours' state.
|
||||
#[derive(Clone, Debug, Default)]
|
||||
pub struct State {
|
||||
/// `RAM_COOLDOWN_TICKS`: 0 when absent.
|
||||
pub ram_cooldown: i32,
|
||||
lower_head_tick: i32,
|
||||
lowering_head: bool,
|
||||
phase: Phase,
|
||||
candidate: Option<Candidate>,
|
||||
reached_at: Option<i32>,
|
||||
/// `RAM_TARGET`, and the direction of the charge.
|
||||
ram_target: Option<Vec3>,
|
||||
ram_direction: Vec3,
|
||||
time: i32,
|
||||
}
|
||||
|
||||
pub fn register(m: &mut Mob) {
|
||||
m.ai.goals.add(2, Ram);
|
||||
}
|
||||
|
||||
/// `Goat.handleEntityEvent`: 58 lowers the head, 59 raises it.
|
||||
pub fn entity_event(m: &mut Mob, event: u8) {
|
||||
match event {
|
||||
58 => m.extra.goat.lowering_head = true,
|
||||
59 => m.extra.goat.lowering_head = false,
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
/// `Goat.aiStep`'s `lowerHeadTick`; a client runs it too.
|
||||
pub fn tick(m: &mut Mob) {
|
||||
let g = &mut m.extra.goat;
|
||||
g.lower_head_tick += if g.lowering_head { 1 } else { -2 };
|
||||
g.lower_head_tick = g.lower_head_tick.clamp(0, 20);
|
||||
if g.ram_cooldown > 0 {
|
||||
g.ram_cooldown -= 1;
|
||||
}
|
||||
}
|
||||
/// `Goat.getRammingXHeadRot`.
|
||||
pub fn ramming_x_head_rot(m: &Mob) -> f32 {
|
||||
let max = if m.is_baby() { 52.5_f32 } else { 30.0 };
|
||||
m.extra.goat.lower_head_tick as f32 / 20.0 * max * (std::f32::consts::PI / 180.0)
|
||||
}
|
||||
/// A server's `level.broadcastEntityEvent(goat, 58 or 59)`, sent when it
|
||||
/// changes what the goat does.
|
||||
fn lower_head(m: &mut Mob, events: &mut Vec<MobEvent>, lower: bool) {
|
||||
if m.extra.goat.lowering_head != lower {
|
||||
let event: i8 = if lower { 58 } else { 59 };
|
||||
entity_event(m, event as u8);
|
||||
let at = [m.position.x, m.position.y, m.position.z];
|
||||
events.push(MobEvent::EntityEvent {
|
||||
mob: m.id,
|
||||
event,
|
||||
at,
|
||||
});
|
||||
}
|
||||
}
|
||||
fn between(m: &mut Mob, (min, max): (i32, i32)) -> i32 {
|
||||
m.random.next_int_between_inclusive(min, max).unwrap_or(min)
|
||||
}
|
||||
fn rams(m: &Mob) -> (i32, i32) {
|
||||
if m.extra.screaming {
|
||||
RAMS_SCREAMER
|
||||
} else {
|
||||
RAMS
|
||||
}
|
||||
}
|
||||
fn block_of(p: Vec3) -> [i32; 3] {
|
||||
[p.x.floor() as i32, p.y.floor() as i32, p.z.floor() as i32]
|
||||
}
|
||||
fn play(m: &Mob, events: &mut Vec<MobEvent>, sound: &'static str, pitch: f32) {
|
||||
if !m.silent {
|
||||
let at = [m.position.x, m.position.y, m.position.z];
|
||||
events.push(MobEvent::Sound(sound, at, 1.0, pitch));
|
||||
}
|
||||
}
|
||||
|
||||
/// `PrepareRamNearestTarget.isWalkableBlock`.
|
||||
fn walkable(m: &Mob, w: &MobWorld, p: [i32; 3]) -> bool {
|
||||
m.stable_destination(w, p) && m.malus(w.path_type_static(p[0], p[1], p[2])) == 0.0
|
||||
}
|
||||
/// `PrepareRamNearestTarget.calculateRammingStartPosition`.
|
||||
fn ramming_start(m: &mut Mob, w: &MobWorld, target: [i32; 3]) -> Option<[i32; 3]> {
|
||||
if !walkable(m, w, target) {
|
||||
return None;
|
||||
}
|
||||
// Direction.Plane.HORIZONTAL: north, east, south, west.
|
||||
let mut starts = Vec::new();
|
||||
for (dx, dz) in [(0, -1), (1, 0), (0, 1), (-1, 0)] {
|
||||
let mut p = target;
|
||||
for _ in 0..MAX_RAM_DISTANCE {
|
||||
p = [p[0] + dx, p[1], p[2] + dz];
|
||||
if !walkable(m, w, p) {
|
||||
p = [p[0] - dx, p[1], p[2] - dz];
|
||||
break;
|
||||
}
|
||||
}
|
||||
let manhattan =
|
||||
(p[0] - target[0]).abs() + (p[1] - target[1]).abs() + (p[2] - target[2]).abs();
|
||||
if manhattan >= MIN_RAM_DISTANCE {
|
||||
starts.push(p);
|
||||
}
|
||||
}
|
||||
let here = block_of(m.position);
|
||||
let sqr = |p: &[i32; 3]| {
|
||||
(0..3)
|
||||
.map(|i| i64::from(p[i] - here[i]).pow(2))
|
||||
.sum::<i64>()
|
||||
};
|
||||
starts.sort_by_key(sqr);
|
||||
starts
|
||||
.into_iter()
|
||||
.find(|&p| m.create_path_to(w, p, 0).is_some_and(|path| path.reached))
|
||||
}
|
||||
/// `PrepareRamNearestTarget.chooseRamPosition`.
|
||||
fn choose(m: &mut Mob, w: &MobWorld, target: Who) {
|
||||
let g = &mut m.extra.goat;
|
||||
g.reached_at = None;
|
||||
let Some(t) = w.target_info(target) else {
|
||||
g.candidate = None;
|
||||
return;
|
||||
};
|
||||
let target_pos = block_of(t.position);
|
||||
m.extra.goat.candidate = ramming_start(m, w, target_pos).map(|start| Candidate {
|
||||
start,
|
||||
target_pos,
|
||||
target,
|
||||
});
|
||||
}
|
||||
/// The nearest living thing the goat sees that `RAM_TARGET_CONDITIONS`
|
||||
/// allow: not a goat, a player who is not creative or a spectator.
|
||||
fn nearest_target(m: &mut Mob, w: &MobWorld) -> Option<Who> {
|
||||
let mut best: Option<(Who, f64)> = None;
|
||||
if let Some(p) = &w.player
|
||||
&& p.alive
|
||||
&& !p.creative
|
||||
&& !p.spectator
|
||||
&& !p.allies.contains(&m.id)
|
||||
&& m.distance_sqr(p.position) <= SENSE_RANGE * SENSE_RANGE
|
||||
&& m.sees(w, Who::Player)
|
||||
{
|
||||
best = Some((Who::Player, m.distance_sqr(p.position)));
|
||||
}
|
||||
let id = m.id;
|
||||
let mobs: Vec<(u32, f64)> = w
|
||||
.nearby(m.position, SENSE_RANGE, |s| {
|
||||
s.id != id && s.species != super::Species::Goat && !s.playing_dead
|
||||
})
|
||||
.map(|s| (s.id, m.distance_sqr(s.position)))
|
||||
.collect();
|
||||
for (mob, d) in mobs {
|
||||
if best.is_none_or(|b| d < b.1) && m.sees(w, Who::Mob(mob)) {
|
||||
best = Some((Who::Mob(mob), d));
|
||||
}
|
||||
}
|
||||
best.map(|b| b.0)
|
||||
}
|
||||
|
||||
/// `PrepareRamNearestTarget` and `RamTarget` of the ram activity.
|
||||
#[derive(Clone, Debug)]
|
||||
struct Ram;
|
||||
impl Goal for Ram {
|
||||
goal_clone!();
|
||||
fn flags(&self) -> u8 {
|
||||
MOVE | LOOK
|
||||
}
|
||||
fn can_use(&mut self, m: &mut Mob, _cx: &mut Cx) -> bool {
|
||||
// The ram activity needs the cooldown to be over (it also waits for
|
||||
// no tempting player and no breed target, which goats lack here).
|
||||
m.extra.goat.ram_cooldown <= 0
|
||||
}
|
||||
fn can_continue(&mut self, m: &mut Mob, cx: &mut Cx) -> bool {
|
||||
let g = &m.extra.goat;
|
||||
match g.phase {
|
||||
Phase::Done => false,
|
||||
Phase::Prepare => {
|
||||
g.time <= PREPARE_TIME_OUT
|
||||
&& g.candidate
|
||||
.is_some_and(|c| cx.w.target_info(c.target).is_some_and(|t| t.alive))
|
||||
}
|
||||
Phase::RamStart | Phase::Ram => g.ram_target.is_some() && g.time <= RAM_TIME_OUT,
|
||||
}
|
||||
}
|
||||
fn start(&mut self, m: &mut Mob, cx: &mut Cx) {
|
||||
m.extra.goat.phase = Phase::Prepare;
|
||||
m.extra.goat.time = 0;
|
||||
m.extra.goat.reached_at = None;
|
||||
m.extra.goat.candidate = None;
|
||||
if let Some(target) = nearest_target(m, cx.w) {
|
||||
choose(m, cx.w, target);
|
||||
}
|
||||
}
|
||||
fn stop(&mut self, m: &mut Mob, cx: &mut Cx) {
|
||||
match m.extra.goat.phase {
|
||||
// PrepareRamNearestTarget.stop: no ram target, so the head comes
|
||||
// up and the cooldown starts.
|
||||
Phase::Prepare => {
|
||||
lower_head(m, cx.events, false);
|
||||
m.extra.goat.ram_cooldown = rams(m).0;
|
||||
}
|
||||
// RamTarget.finishRam (also what its timeout leaves undone).
|
||||
Phase::RamStart | Phase::Ram => finish_ram(m, cx.events),
|
||||
Phase::Done => {}
|
||||
}
|
||||
m.extra.goat.phase = Phase::Done;
|
||||
m.extra.goat.candidate = None;
|
||||
}
|
||||
fn every_tick(&self) -> bool {
|
||||
true
|
||||
}
|
||||
fn tick(&mut self, m: &mut Mob, cx: &mut Cx) {
|
||||
m.extra.goat.time += 1;
|
||||
match m.extra.goat.phase {
|
||||
Phase::Prepare => prepare_tick(m, cx),
|
||||
Phase::RamStart => {
|
||||
// RamTarget.start: charge from here at the ram target.
|
||||
let (Some(target), here) = (m.extra.goat.ram_target, block_of(m.position)) else {
|
||||
return;
|
||||
};
|
||||
let d = Vec3::new(
|
||||
f64::from(here[0]) - target.x,
|
||||
0.0,
|
||||
f64::from(here[2]) - target.z,
|
||||
);
|
||||
m.extra.goat.ram_direction = d.normalize();
|
||||
m.extra.goat.phase = Phase::Ram;
|
||||
m.extra.goat.time = 0;
|
||||
m.nav_move_to_entity(cx.w, [target.x, target.y, target.z], 0, 3.0);
|
||||
ram_tick(m, cx);
|
||||
}
|
||||
Phase::Ram => ram_tick(m, cx),
|
||||
Phase::Done => {}
|
||||
}
|
||||
}
|
||||
}
|
||||
/// `PrepareRamNearestTarget.tick`.
|
||||
fn prepare_tick(m: &mut Mob, cx: &mut Cx) {
|
||||
let Some(c) = m.extra.goat.candidate else {
|
||||
return;
|
||||
};
|
||||
let Some(t) = cx.w.target_info(c.target) else {
|
||||
return;
|
||||
};
|
||||
m.look_at([t.position.x, t.eye_y, t.position.z]);
|
||||
let now = m.age as i32;
|
||||
let at = [
|
||||
f64::from(c.start[0]) + 0.5,
|
||||
f64::from(c.start[1]),
|
||||
f64::from(c.start[2]) + 0.5,
|
||||
];
|
||||
if m.ai.nav.is_done() && block_of(m.position) != c.start {
|
||||
m.nav_move_to_entity(cx.w, at, 0, 1.25);
|
||||
}
|
||||
if block_of(t.position) != c.target_pos {
|
||||
// The target moved: the head comes up and a new spot is chosen.
|
||||
lower_head(m, cx.events, false);
|
||||
m.nav_stop();
|
||||
choose(m, cx.w, c.target);
|
||||
} else if block_of(m.position) == c.start {
|
||||
lower_head(m, cx.events, true);
|
||||
let since = *m.extra.goat.reached_at.get_or_insert(now);
|
||||
if i64::from(now - since) >= RAM_PREPARE_TIME {
|
||||
// getEdgeOfBlock.
|
||||
let (x, z) = (
|
||||
f64::from(c.target_pos[0])
|
||||
+ 0.5
|
||||
+ 0.5 * f64::from(sign(c.target_pos[0] - c.start[0])),
|
||||
f64::from(c.target_pos[2])
|
||||
+ 0.5
|
||||
+ 0.5 * f64::from(sign(c.target_pos[2] - c.start[2])),
|
||||
);
|
||||
m.extra.goat.ram_target = Some(Vec3::new(x, f64::from(c.target_pos[1]), z));
|
||||
let sound = if m.extra.screaming {
|
||||
"entity.goat.screaming.prepare_ram"
|
||||
} else {
|
||||
"entity.goat.prepare_ram"
|
||||
};
|
||||
let pitch = m.voice_pitch();
|
||||
play(m, cx.events, sound, pitch);
|
||||
m.extra.goat.candidate = None;
|
||||
m.extra.goat.phase = Phase::RamStart;
|
||||
}
|
||||
}
|
||||
}
|
||||
fn sign(v: i32) -> i32 {
|
||||
v.signum()
|
||||
}
|
||||
/// `RamTarget.tick`.
|
||||
fn ram_tick(m: &mut Mob, cx: &mut Cx) {
|
||||
let impact = if m.extra.screaming {
|
||||
"entity.goat.screaming.ram_impact"
|
||||
} else {
|
||||
"entity.goat.ram_impact"
|
||||
};
|
||||
let from = [m.position.x, m.position.y, m.position.z];
|
||||
if let Some(target) = rammed(m, cx.w) {
|
||||
// hurtServer by `noAggroMobAttack`, then the knockback.
|
||||
let amount = m.attrs.attack_damage;
|
||||
match target {
|
||||
Who::Player => cx.events.push(MobEvent::DamagePlayer {
|
||||
amount,
|
||||
from,
|
||||
kind: "minecraft:mob_attack_no_aggro",
|
||||
}),
|
||||
Who::Mob(id) => cx.events.push(MobEvent::AttackMob {
|
||||
target: id,
|
||||
amount,
|
||||
source: Damage::mob(m.id, from),
|
||||
}),
|
||||
}
|
||||
let level = |effect: &str| {
|
||||
m.extra
|
||||
.effects
|
||||
.iter()
|
||||
.find(|e| e.0 == effect)
|
||||
.map_or(0, |e| e.2 + 1)
|
||||
};
|
||||
let boost = 0.25 * (level("minecraft:speed") - level("minecraft:slowness")) as f32;
|
||||
let factor = (m.ai.speed * 1.65).clamp(0.2, 3.0) + boost;
|
||||
let force = if m.is_baby() { 1.0 } else { 2.5 };
|
||||
let d = m.extra.goat.ram_direction;
|
||||
cx.events.push(MobEvent::Knockback {
|
||||
target,
|
||||
power: f64::from(factor * force),
|
||||
x: d.x,
|
||||
z: d.z,
|
||||
});
|
||||
finish_ram(m, cx.events);
|
||||
play(m, cx.events, impact, 1.0);
|
||||
} else if rammed_horn_block(m, cx.w) {
|
||||
play(m, cx.events, impact, 1.0);
|
||||
if drop_horn(m, cx.events) {
|
||||
play(m, cx.events, "entity.goat.horn_break", 1.0);
|
||||
}
|
||||
finish_ram(m, cx.events);
|
||||
} else if m.ai.nav.is_done() || m.extra.goat.ram_target.is_none() {
|
||||
// The walk target is gone: the charge ended where it could.
|
||||
finish_ram(m, cx.events);
|
||||
}
|
||||
}
|
||||
/// The first living thing in the goat's box that `RAM_TARGET_CONDITIONS`
|
||||
/// allow.
|
||||
fn rammed(m: &Mob, w: &MobWorld) -> Option<Who> {
|
||||
let bounds = m.bounds();
|
||||
if let Some(p) = &w.player
|
||||
&& p.alive
|
||||
&& !p.creative
|
||||
&& !p.spectator
|
||||
&& !p.allies.contains(&m.id)
|
||||
&& p.bounds.intersects(bounds)
|
||||
{
|
||||
return Some(Who::Player);
|
||||
}
|
||||
w.others_near(bounds)
|
||||
.find(|s| {
|
||||
s.id != m.id
|
||||
&& s.alive
|
||||
&& s.species != super::Species::Goat
|
||||
&& !s.playing_dead
|
||||
&& s.bounds.intersects(bounds)
|
||||
})
|
||||
.map(|s| Who::Mob(s.id))
|
||||
}
|
||||
/// `RamTarget.hasRammedHornBreakingBlock`: the block one step ahead, or the
|
||||
/// one above it, snaps a horn.
|
||||
fn rammed_horn_block(m: &Mob, w: &MobWorld) -> bool {
|
||||
let ahead = Vec3::new(m.velocity.x, 0.0, m.velocity.z).normalize();
|
||||
let at = block_of(m.position.add_vector(ahead));
|
||||
let snaps =
|
||||
|p: [i32; 3]| w.has_tag("minecraft:snaps_goat_horn", w.block_name(p[0], p[1], p[2]));
|
||||
snaps(at) || snaps([at[0], at[1] + 1, at[2]])
|
||||
}
|
||||
/// `Goat.dropHorn`: one of the horns it still has comes off and drops.
|
||||
fn drop_horn(m: &mut Mob, events: &mut Vec<MobEvent>) -> bool {
|
||||
if m.is_baby() {
|
||||
return false;
|
||||
}
|
||||
let [left_lost, right_lost] = m.extra.lost_horns;
|
||||
if left_lost && right_lost {
|
||||
return false;
|
||||
}
|
||||
let lose_left = if left_lost {
|
||||
false
|
||||
} else if right_lost {
|
||||
true
|
||||
} else {
|
||||
m.random.next_boolean()
|
||||
};
|
||||
m.extra.lost_horns[usize::from(!lose_left)] = true;
|
||||
events.push(MobEvent::DropItem {
|
||||
item: "minecraft:goat_horn".into(),
|
||||
count: 1,
|
||||
at: [m.position.x, m.position.y, m.position.z],
|
||||
});
|
||||
true
|
||||
}
|
||||
/// `RamTarget.finishRam`.
|
||||
fn finish_ram(m: &mut Mob, events: &mut Vec<MobEvent>) {
|
||||
lower_head(m, events, false);
|
||||
let wait = rams(m);
|
||||
m.extra.goat.ram_cooldown = between(m, wait);
|
||||
m.extra.goat.ram_target = None;
|
||||
m.extra.goat.phase = Phase::Done;
|
||||
}
|
||||
@@ -157,12 +157,16 @@ impl Goal for OfferFlower {
|
||||
fn can_continue(&mut self, _: &mut Mob, _: &mut Cx) -> bool {
|
||||
self.ticks > 0
|
||||
}
|
||||
fn start(&mut self, m: &mut Mob, _: &mut Cx) {
|
||||
fn start(&mut self, m: &mut Mob, cx: &mut Cx) {
|
||||
self.ticks = 400;
|
||||
m.extra.counter = 400;
|
||||
// IronGolem.offerFlower(true): entity event 11.
|
||||
entity_event(m, 11, cx);
|
||||
}
|
||||
fn stop(&mut self, m: &mut Mob, cx: &mut Cx) {
|
||||
m.extra.counter = 0;
|
||||
// IronGolem.offerFlower(false): entity event 34.
|
||||
entity_event(m, 34, cx);
|
||||
// A copper golem the offer ran out beside takes the poppy on its
|
||||
// antenna (#accepts_iron_golem_gift) when it has none.
|
||||
if self.ticks == 0
|
||||
@@ -198,6 +202,7 @@ impl Goal for OfferFlower {
|
||||
/// the target is thrown upward.
|
||||
pub fn iron_hurt_target(m: &mut Mob, who: Who, cx: &mut Cx) {
|
||||
m.extra.anim_ticks = 10;
|
||||
entity_event(m, 4, cx);
|
||||
let attack = m.attrs.attack_damage;
|
||||
let amount = if attack as i32 > 0 {
|
||||
attack / 2.0 + m.random.next_int_bound(attack as i32).unwrap_or(0) as f32
|
||||
@@ -224,6 +229,33 @@ pub fn iron_hurt_target(m: &mut Mob, who: Who, cx: &mut Cx) {
|
||||
1.0,
|
||||
));
|
||||
}
|
||||
/// `Level.broadcastEntityEvent` for an iron golem (guests run `client_event`).
|
||||
fn entity_event(m: &Mob, event: i8, cx: &mut Cx) {
|
||||
cx.events.push(MobEvent::EntityEvent {
|
||||
mob: m.id,
|
||||
event,
|
||||
at: [m.position.x, m.position.y, m.position.z],
|
||||
});
|
||||
}
|
||||
/// `IronGolem.handleEntityEvent` on a client: 4 the swing, 11 and 34 the
|
||||
/// poppy offer starting and stopping.
|
||||
pub fn client_event(m: &mut Mob, event: u8) {
|
||||
match event {
|
||||
4 => m.extra.anim_ticks = 10,
|
||||
11 => m.extra.counter = 400,
|
||||
34 => m.extra.counter = 0,
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
/// The swing and offer clocks of `IronGolem.aiStep` on a client.
|
||||
pub fn client_tick(m: &mut Mob) {
|
||||
if m.extra.anim_ticks > 0 {
|
||||
m.extra.anim_ticks -= 1;
|
||||
}
|
||||
if m.extra.counter > 0 {
|
||||
m.extra.counter -= 1;
|
||||
}
|
||||
}
|
||||
/// `IronGolem.aiStep` timers and anger.
|
||||
pub fn iron_ai_step(m: &mut Mob) {
|
||||
if m.extra.anim_ticks > 0 {
|
||||
|
||||
@@ -214,12 +214,9 @@ pub fn travel(m: &mut Mob, w: &MobWorld, input: Vec3, events: &mut Vec<MobEvent>
|
||||
}
|
||||
true
|
||||
}
|
||||
/// `Guardian.aiStep`: the client animations, air, flopping on land, the
|
||||
/// beam's clock, and the spikes' thorns.
|
||||
pub fn ai_step(m: &mut Mob, w: &MobWorld, events: &mut Vec<MobEvent>) {
|
||||
if !m.alive() {
|
||||
return;
|
||||
}
|
||||
/// `Guardian.aiStep`'s `clientSideTailAnimation` and
|
||||
/// `clientSideSpikesAnimation` (the server's and a client's).
|
||||
pub fn tail_and_spikes(m: &mut Mob) {
|
||||
let moving = m.extra.flag2;
|
||||
let in_water = m.fluids.in_water;
|
||||
// Tail and spikes (clientSideTailAnimation / clientSideSpikesAnimation).
|
||||
@@ -244,6 +241,16 @@ pub fn ai_step(m: &mut Mob, w: &MobWorld, events: &mut Vec<MobEvent>) {
|
||||
} else {
|
||||
a.v[SPIKES] += (1.0 - a.v[SPIKES]) * 0.06;
|
||||
}
|
||||
}
|
||||
/// `Guardian.aiStep`: the client animations, air, flopping on land, the
|
||||
/// beam's clock, and the spikes' thorns.
|
||||
pub fn ai_step(m: &mut Mob, w: &MobWorld, events: &mut Vec<MobEvent>) {
|
||||
if !m.alive() {
|
||||
return;
|
||||
}
|
||||
let moving = m.extra.flag2;
|
||||
let in_water = m.fluids.in_water;
|
||||
tail_and_spikes(m);
|
||||
client_tick(m);
|
||||
if in_water {
|
||||
m.air = 300;
|
||||
@@ -276,17 +283,7 @@ pub fn ai_step(m: &mut Mob, w: &MobWorld, events: &mut Vec<MobEvent>) {
|
||||
None => w.player.as_ref().map(|p| p.eye),
|
||||
};
|
||||
if let Some(look) = look {
|
||||
m.anim.v[EYE_Y] = if look[1] - m.eye_y() > 0.0 { 0.0 } else { 1.0 };
|
||||
let yaw = m.head_yaw.to_radians();
|
||||
let pitch = m.head_pitch.to_radians();
|
||||
let view = [-yaw.msin() * pitch.mcos(), yaw.mcos() * pitch.mcos()];
|
||||
let d = [m.position.x - look[0], m.position.z - look[2]];
|
||||
let len = (d[0] * d[0] + d[1] * d[1]).sqrt().max(1e-9);
|
||||
let (dx, dz) = ((d[0] / len) as f32, (d[1] / len) as f32);
|
||||
// Vec3.yRot(pi / 2) of the direction from the target.
|
||||
let side = [dz, -dx];
|
||||
let dot = view[0] * side[0] + view[1] * side[1];
|
||||
m.anim.v[EYE_X] = dot.abs().sqrt() * 2.0 * dot.signum();
|
||||
look_at(m, look);
|
||||
}
|
||||
// Guardian.hurtServer: a still guardian's spikes hurt a melee attacker.
|
||||
if m.hurt_time == 10
|
||||
@@ -317,6 +314,21 @@ pub fn ai_step(m: &mut Mob, w: &MobWorld, events: &mut Vec<MobEvent>) {
|
||||
}
|
||||
}
|
||||
}
|
||||
/// `GuardianModel.setupAnim`'s eye: it follows `look` (the attack target's
|
||||
/// eye, else the camera's), from where the guardian faces.
|
||||
pub fn look_at(m: &mut Mob, look: [f64; 3]) {
|
||||
m.anim.v[EYE_Y] = if look[1] - m.eye_y() > 0.0 { 0.0 } else { 1.0 };
|
||||
let yaw = m.head_yaw.to_radians();
|
||||
let pitch = m.head_pitch.to_radians();
|
||||
let view = [-yaw.msin() * pitch.mcos(), yaw.mcos() * pitch.mcos()];
|
||||
let d = [m.position.x - look[0], m.position.z - look[2]];
|
||||
let len = (d[0] * d[0] + d[1] * d[1]).sqrt().max(1e-9);
|
||||
let (dx, dz) = ((d[0] / len) as f32, (d[1] / len) as f32);
|
||||
// Vec3.yRot(pi / 2) of the direction from the target.
|
||||
let side = [dz, -dx];
|
||||
let dot = view[0] * side[0] + view[1] * side[1];
|
||||
m.anim.v[EYE_X] = dot.abs().sqrt() * 2.0 * dot.signum();
|
||||
}
|
||||
/// `ElderGuardian.customServerAiStep`: every minute, mining fatigue III for
|
||||
/// players within 50 blocks; a home within 16 blocks.
|
||||
pub fn elder_server_ai_step(m: &mut Mob, cx: &mut Cx) {
|
||||
|
||||
@@ -544,14 +544,15 @@ pub fn ai_step(m: &mut Mob, w: &MobWorld) {
|
||||
m.ai.home = Some((here, radius));
|
||||
}
|
||||
}
|
||||
// continuousHeal: a point every 30 seconds, every second in rain.
|
||||
// continuousHeal: a point every 30 seconds, every second in rain or snow
|
||||
// (Level.precipitationAt).
|
||||
if m.alive() && m.death_time == 0 && m.health < m.attrs.max_health {
|
||||
let (x, y, z) = (
|
||||
m.position.x.floor() as i32,
|
||||
m.position.y.floor() as i32,
|
||||
m.position.z.floor() as i32,
|
||||
);
|
||||
let wet = w.raining && w.can_see_sky(x, y, z);
|
||||
let wet = w.precipitation_at(x, y, z).is_some();
|
||||
let interval = if wet { 20 } else { 600 };
|
||||
if m.age.is_multiple_of(interval) {
|
||||
m.health = (m.health + 1.0).min(m.attrs.max_health);
|
||||
|
||||
@@ -165,6 +165,12 @@ impl AnimationState {
|
||||
pub fn stop(&mut self) {
|
||||
self.started = None;
|
||||
}
|
||||
/// `AnimationState.fastForward`: the animation is `ticks` further on.
|
||||
pub fn fast_forward(&mut self, ticks: i32) {
|
||||
if let Some(s) = &mut self.started {
|
||||
*s -= ticks as f32;
|
||||
}
|
||||
}
|
||||
pub fn animate_when(&mut self, condition: bool, age: f32) {
|
||||
if condition {
|
||||
self.start_if_stopped(age)
|
||||
|
||||
@@ -9,8 +9,15 @@ use crate::{
|
||||
};
|
||||
use horizon_core::{
|
||||
geometry::{Aabb, Vec3, trig},
|
||||
position::BlockPos,
|
||||
random::RandomSource,
|
||||
};
|
||||
use horizon_world::{
|
||||
entity_fluid::{CurrentAccumulator, CurrentTarget},
|
||||
fluid::{FluidKind, FluidState},
|
||||
fluid_flow::{FluidFlowWorld, flow_at},
|
||||
registry::BlockStateId,
|
||||
};
|
||||
|
||||
/// `Entity.moveDist`, `flyDist` and `nextStep`.
|
||||
#[derive(Clone, Copy, Debug)]
|
||||
@@ -36,6 +43,50 @@ pub struct Fluids {
|
||||
pub in_lava: bool,
|
||||
pub eye_in_water: bool,
|
||||
}
|
||||
/// `FluidFlowWorld` over the mob's scene: the neighbours a flow vector reads.
|
||||
struct FlowScene<'a, 'w>(&'a MobWorld<'w>);
|
||||
impl FluidFlowWorld for FlowScene<'_, '_> {
|
||||
type Error = std::convert::Infallible;
|
||||
type Block = BlockStateId;
|
||||
fn fluid(&mut self, p: BlockPos) -> Result<FluidState, Self::Error> {
|
||||
Ok(self.0.fluid_tables.fluid(self.0.world.block(p.x, p.y, p.z)))
|
||||
}
|
||||
fn block(&mut self, p: BlockPos) -> Result<BlockStateId, Self::Error> {
|
||||
Ok(self.0.world.block(p.x, p.y, p.z))
|
||||
}
|
||||
fn blocks_fluid_flow(&self, s: BlockStateId) -> bool {
|
||||
self.0.fluid_tables.blocks_flow(s)
|
||||
}
|
||||
fn is_ice_class(&self, s: BlockStateId) -> bool {
|
||||
self.0.fluid_tables.ice(s)
|
||||
}
|
||||
fn is_face_sturdy(
|
||||
&mut self,
|
||||
s: BlockStateId,
|
||||
_: BlockPos,
|
||||
d: horizon_core::geometry::Direction,
|
||||
) -> Result<bool, Self::Error> {
|
||||
Ok(self.0.fluid_tables.sturdy(s, d))
|
||||
}
|
||||
}
|
||||
/// The mob's velocity, for `CurrentAccumulator::apply_to`.
|
||||
struct Pushed<'a>(&'a mut Vec3);
|
||||
impl CurrentTarget for Pushed<'_> {
|
||||
type Error = std::convert::Infallible;
|
||||
fn is_player_class(&self) -> bool {
|
||||
false
|
||||
}
|
||||
fn velocity(&mut self) -> Result<Vec3, Self::Error> {
|
||||
Ok(*self.0)
|
||||
}
|
||||
fn add_delta_movement(&mut self, delta: Vec3) -> Result<(), Self::Error> {
|
||||
let sum = Vec3::new(self.0.x + delta.x, self.0.y + delta.y, self.0.z + delta.z);
|
||||
if delta.is_finite() && sum.is_finite() {
|
||||
*self.0 = sum;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
pub fn wrap(a: f32) -> f32 {
|
||||
let mut a = a % 360.0;
|
||||
if a >= 180.0 {
|
||||
@@ -140,6 +191,40 @@ impl MobWorld<'_> {
|
||||
}
|
||||
out
|
||||
}
|
||||
/// Entity.updateFluidHeightAndDoFluidPushing's currents over the deflated
|
||||
/// box: the flow of every water and lava block it touches, summed (a thin
|
||||
/// fluid layer scales its flow down), as (water, lava).
|
||||
fn fluid_currents(&self, bounds: Aabb) -> (CurrentAccumulator, CurrentAccumulator) {
|
||||
let base_y = bounds.min_position().y;
|
||||
let b = bounds.deflate(Vec3::new(0.001, 0.001, 0.001));
|
||||
let (lo, hi) = (b.min_position(), b.max_position());
|
||||
let mut scene = FlowScene(self);
|
||||
let (mut water, mut lava) = (CurrentAccumulator::default(), CurrentAccumulator::default());
|
||||
for x in lo.x.floor() as i32..=(hi.x.ceil() as i32 - 1) {
|
||||
for y in lo.y.floor() as i32..=(hi.y.ceil() as i32 - 1) {
|
||||
for z in lo.z.floor() as i32..=(hi.z.ceil() as i32 - 1) {
|
||||
let p = BlockPos::new(x, y, z);
|
||||
let state = self.fluid_tables.fluid(self.world.block(x, y, z));
|
||||
let current = match state.kind() {
|
||||
FluidKind::Empty => continue,
|
||||
FluidKind::Water | FluidKind::FlowingWater => &mut water,
|
||||
FluidKind::Lava | FluidKind::FlowingLava => &mut lava,
|
||||
};
|
||||
let surface = f64::from(y) + self.fluid_height(x, y, z).1;
|
||||
if surface < lo.y {
|
||||
continue;
|
||||
}
|
||||
current.height = current.height.max(surface - base_y);
|
||||
let mut flow = flow_at(&mut scene, state, p).unwrap_or(Vec3::ZERO);
|
||||
if current.height < 0.4 {
|
||||
flow = flow.scale(current.height);
|
||||
}
|
||||
current.accumulate(flow);
|
||||
}
|
||||
}
|
||||
}
|
||||
(water, lava)
|
||||
}
|
||||
/// `BlockState.getCollisionShape(level, pos)` (the empty collision
|
||||
/// context) as boxes in the world.
|
||||
pub fn block_collision_boxes(&self, x: i32, y: i32, z: i32) -> Vec<Aabb> {
|
||||
@@ -235,6 +320,18 @@ impl MobWorld<'_> {
|
||||
};
|
||||
scene.unobstructed(b).unwrap_or(false)
|
||||
}
|
||||
/// `Entity.isFree`: no collision and `LevelReader.containsAnyLiquid` false
|
||||
/// over the blocks from the box's minimum corner to its maximum corner.
|
||||
pub fn is_free(&self, b: Aabb) -> bool {
|
||||
let (lo, hi) = (b.min_position(), b.max_position());
|
||||
let liquid = (lo.x.floor() as i32..=hi.x.floor() as i32).any(|x| {
|
||||
(lo.y.floor() as i32..=hi.y.floor() as i32).any(|y| {
|
||||
(lo.z.floor() as i32..=hi.z.floor() as i32)
|
||||
.any(|z| self.facts.get(self.world.block(x, y, z)).fluid != Fluid::None)
|
||||
})
|
||||
});
|
||||
!liquid && self.no_collision(b)
|
||||
}
|
||||
}
|
||||
impl Mob {
|
||||
/// `Entity.refreshDimensions` after growing from `old_w` by `old_h`:
|
||||
@@ -281,6 +378,15 @@ impl Mob {
|
||||
return (f64::from(0.2_f32), f64::from(0.2_f32), f64::from(0.2_f32));
|
||||
}
|
||||
let k = &self.kind;
|
||||
// Avatar.POSES: a player's or mannequin's box follows its pose.
|
||||
if matches!(&*k.id, "minecraft:player" | "minecraft:mannequin") {
|
||||
let d = self.extra.avatar_pose.dimensions(1.0);
|
||||
return (
|
||||
f64::from(d.width),
|
||||
f64::from(d.height),
|
||||
f64::from(d.eye_height),
|
||||
);
|
||||
}
|
||||
let (mut w, mut h, mut e) = (k.width as f32, k.height as f32, k.eye_height as f32);
|
||||
// Sniffer.DIGGING_DIMENSIONS: 0.4 lower, its eyes at 0.81.
|
||||
if k.species == super::Species::Sniffer && super::sniffer::digging(self) {
|
||||
@@ -334,6 +440,27 @@ impl Mob {
|
||||
pub fn is_in_liquid(&self) -> bool {
|
||||
self.fluids.in_water || self.fluids.in_lava
|
||||
}
|
||||
/// `Entity.isPushedByFluid`: water animals, turtles, axolotls, frogs and
|
||||
/// nautiluses keep their own course.
|
||||
fn pushed_by_fluid(&self) -> bool {
|
||||
use super::Species as S;
|
||||
!matches!(
|
||||
self.species(),
|
||||
S::Tadpole
|
||||
| S::Cod
|
||||
| S::Salmon
|
||||
| S::TropicalFish
|
||||
| S::Pufferfish
|
||||
| S::Squid
|
||||
| S::GlowSquid
|
||||
| S::Dolphin
|
||||
| S::Turtle
|
||||
| S::Axolotl
|
||||
| S::Frog
|
||||
| S::Nautilus
|
||||
| S::ZombieNautilus
|
||||
)
|
||||
}
|
||||
fn affected_by_fluids(&self) -> bool {
|
||||
true
|
||||
}
|
||||
@@ -428,6 +555,17 @@ impl Mob {
|
||||
// Entity.checkSupportingBlock
|
||||
self.supporting = self.supporting_block(w, moved);
|
||||
self.movement_emission(w, moved, events);
|
||||
// Entity.applyEffectsFromBlocks: bubble columns push the mob.
|
||||
if crate::motion::bubble_column_push(
|
||||
w.world,
|
||||
w.registry,
|
||||
w.facts,
|
||||
w.collision,
|
||||
self.bounds(),
|
||||
&mut self.velocity,
|
||||
) {
|
||||
self.fall_distance = 0.0;
|
||||
}
|
||||
}
|
||||
/// LivingEntity.checkFallDamage: the fluid state refreshed out of water
|
||||
/// (touching water resets the fall), then Entity.checkFallDamage.
|
||||
@@ -450,6 +588,22 @@ impl Mob {
|
||||
pub(super) fn update_fluids(&mut self, w: &MobWorld, events: &mut Vec<MobEvent>) {
|
||||
let was_touching_water = self.fluids.in_water;
|
||||
self.fluids = w.fluids(self.bounds(), self.eye_y());
|
||||
// EntityFluidInteraction.applyCurrentTo: the flow pushes the mob.
|
||||
if (self.fluids.in_water || self.fluids.in_lava) && self.pushed_by_fluid() {
|
||||
let (water, lava) = w.fluid_currents(self.bounds());
|
||||
let lava_scale = if w.nether {
|
||||
0.007
|
||||
} else {
|
||||
0.0023333333333333335
|
||||
};
|
||||
let mut pushed = Pushed(&mut self.velocity);
|
||||
if water.height > 0.0 {
|
||||
let _ = water.apply_to(&mut pushed, 0.014);
|
||||
}
|
||||
if lava.height > 0.0 {
|
||||
let _ = lava.apply_to(&mut pushed, lava_scale);
|
||||
}
|
||||
}
|
||||
if self.fluids.in_water {
|
||||
self.fall_distance = 0.0;
|
||||
if !was_touching_water && !self.first_tick {
|
||||
@@ -529,13 +683,15 @@ impl Mob {
|
||||
}
|
||||
}
|
||||
/// `Entity.nextStep`: the next whole metre walked; a warden's steps come
|
||||
/// every 0.55, a strider's every 0.6.
|
||||
/// every 0.55, a strider's every 0.6, a turtle's every 0.15.
|
||||
fn next_step(&self) -> f32 {
|
||||
let walked = self.ai.steps.move_dist;
|
||||
if self.kind.species == super::Species::Warden {
|
||||
walked + 0.55
|
||||
} else if self.kind.species == super::Species::Strider {
|
||||
walked + 0.6
|
||||
} else if self.kind.species == super::Species::Turtle {
|
||||
walked + 0.15
|
||||
} else {
|
||||
(walked as i32).wrapping_add(1) as f32
|
||||
}
|
||||
@@ -816,7 +972,7 @@ impl Mob {
|
||||
// jumpOutOfFluid
|
||||
let m = self.velocity;
|
||||
if self.horizontal_collision
|
||||
&& w.no_collision(self.bounds().move_by(Vec3::new(
|
||||
&& w.is_free(self.bounds().move_by(Vec3::new(
|
||||
m.x,
|
||||
m.y + f64::from(0.6_f32) - self.position.y + old_y,
|
||||
m.z,
|
||||
|
||||
File diff suppressed because it is too large.
Load diff
@@ -469,6 +469,12 @@ fn find_piglin_repellent(m: &Mob, w: &MobWorld) -> Option<[i32; 3]> {
|
||||
/// `HoglinBase.hurtAndThrowTarget`.
|
||||
pub fn hoglin_hurt_target(m: &mut Mob, who: Who, cx: &mut Cx) {
|
||||
m.extra.anim_ticks = 10;
|
||||
// Entity event 4: clients start the headbutt.
|
||||
cx.events.push(MobEvent::EntityEvent {
|
||||
mob: m.id,
|
||||
event: 4,
|
||||
at: [m.position.x, m.position.y, m.position.z],
|
||||
});
|
||||
let attack = m.attrs.attack_damage;
|
||||
let amount = if !m.is_baby() && attack as i32 > 0 {
|
||||
attack / 2.0 + m.random.next_int_bound(attack as i32).unwrap_or(0) as f32
|
||||
@@ -603,6 +609,19 @@ pub fn piglin_pose(
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
/// `Hoglin.handleEntityEvent` and `Zoglin`'s event 4 on a client: the
|
||||
/// headbutt, which `aiStep` then counts down.
|
||||
pub fn client_event(m: &mut Mob, event: u8) {
|
||||
if event == 4 {
|
||||
m.extra.anim_ticks = 10;
|
||||
}
|
||||
}
|
||||
/// The headbutt's countdown (`Hoglin.aiStep`, `Zoglin.aiStep`) on a client.
|
||||
pub fn client_tick(m: &mut Mob) {
|
||||
if m.extra.anim_ticks > 0 {
|
||||
m.extra.anim_ticks -= 1;
|
||||
}
|
||||
}
|
||||
/// `HoglinModel.setupAnim`.
|
||||
pub fn hoglin_anim(m: &Mob, s: &State, p: &mut Poses) {
|
||||
let (pos, sp) = (s.pos, s.speed);
|
||||
|
||||
@@ -49,9 +49,14 @@ pub fn register(m: &mut Mob) {
|
||||
pub fn immobile(m: &Mob) -> bool {
|
||||
m.extra.counter > 0 || m.extra.counter2 > 0 || m.extra.anim_ticks > 0
|
||||
}
|
||||
/// `Ravager.doHurtTarget`: the bite animation.
|
||||
/// `Ravager.doHurtTarget`: the bite animation (entity event 4 for clients).
|
||||
pub fn on_attack(m: &mut Mob, events: &mut Vec<MobEvent>) {
|
||||
m.extra.counter = 10;
|
||||
events.push(MobEvent::EntityEvent {
|
||||
mob: m.id,
|
||||
event: 4,
|
||||
at: [m.position.x, m.position.y, m.position.z],
|
||||
});
|
||||
events.push(MobEvent::Sound(
|
||||
"entity.ravager.attack",
|
||||
[m.position.x, m.position.y, m.position.z],
|
||||
@@ -67,6 +72,12 @@ pub fn blocked(m: &mut Mob, events: &mut Vec<MobEvent>) -> bool {
|
||||
}
|
||||
if m.random.next_double() < 0.5 {
|
||||
m.extra.counter2 = 40;
|
||||
// Entity event 39: the stun starts on clients.
|
||||
events.push(MobEvent::EntityEvent {
|
||||
mob: m.id,
|
||||
event: 39,
|
||||
at: [m.position.x, m.position.y, m.position.z],
|
||||
});
|
||||
events.push(MobEvent::Sound(
|
||||
"entity.ravager.stunned",
|
||||
[m.position.x, m.position.y, m.position.z],
|
||||
@@ -155,6 +166,34 @@ pub fn ai_step(m: &mut Mob, w: &MobWorld, events: &mut Vec<MobEvent>) {
|
||||
}
|
||||
}
|
||||
}
|
||||
/// `Ravager.handleEntityEvent` 4 and 39 on a client.
|
||||
pub fn entity_event(m: &mut Mob, event: u8) {
|
||||
match event {
|
||||
4 => m.extra.counter = 10,
|
||||
39 => m.extra.counter2 = 40,
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
/// `Ravager.aiStep`'s clocks on a client: the bite and roar run down, and
|
||||
/// the end of the stun starts the roar (the sounds, particles and the roar's
|
||||
/// damage are the server's).
|
||||
pub fn client_tick(m: &mut Mob) {
|
||||
if !m.alive() {
|
||||
return;
|
||||
}
|
||||
if m.extra.anim_ticks > 0 {
|
||||
m.extra.anim_ticks -= 1;
|
||||
}
|
||||
if m.extra.counter > 0 {
|
||||
m.extra.counter -= 1;
|
||||
}
|
||||
if m.extra.counter2 > 0 {
|
||||
m.extra.counter2 -= 1;
|
||||
if m.extra.counter2 == 0 {
|
||||
m.extra.anim_ticks = 20;
|
||||
}
|
||||
}
|
||||
}
|
||||
/// `Ravager.roar`: 6 damage to everything but illagers within 4 blocks,
|
||||
/// and a strong knockback (players too, through the client).
|
||||
fn roar(m: &mut Mob, w: &MobWorld, events: &mut Vec<MobEvent>) {
|
||||
|
||||
@@ -137,6 +137,10 @@ pub fn body(m: &Mob) -> (String, String) {
|
||||
baby_or(m, "wolf#main", "wolf_baby#main"),
|
||||
super::tame::texture(m),
|
||||
),
|
||||
S::Panda => (
|
||||
baby_or(m, "panda#main", "panda_baby#main"),
|
||||
super::panda::texture(m),
|
||||
),
|
||||
S::HappyGhast => (
|
||||
baby_or(m, "happy_ghast#main", "happy_ghast_baby#main"),
|
||||
if baby {
|
||||
@@ -177,10 +181,87 @@ pub fn body(m: &Mob) -> (String, String) {
|
||||
"frog#main".into(),
|
||||
format!("entity/frog/frog_{}", climate(m)),
|
||||
),
|
||||
S::Rabbit => (
|
||||
baby_or(m, "rabbit#main", "rabbit_baby#main"),
|
||||
// RabbitRenderer.getTextureLocation: the Toast name tag, else the
|
||||
// variant (the killer bunny's id is 99).
|
||||
S::Rabbit => {
|
||||
let name = if m.custom_name.as_deref() == Some("Toast") {
|
||||
"toast"
|
||||
} else {
|
||||
match m.extra.variant {
|
||||
1 => "white",
|
||||
2 => "black",
|
||||
3 => "white_splotched",
|
||||
4 => "gold",
|
||||
5 => "salt",
|
||||
99 => "caerbannog",
|
||||
_ => "brown",
|
||||
}
|
||||
};
|
||||
(
|
||||
baby_or(m, "rabbit#main", "rabbit_baby#main"),
|
||||
b(&format!("entity/rabbit/rabbit_{name}")),
|
||||
)
|
||||
}
|
||||
// ParrotRenderer.getVariantTexture.
|
||||
S::Parrot => {
|
||||
let name = ["red_blue", "blue", "green", "yellow_blue", "grey"]
|
||||
[usize::from(m.extra.variant).min(4)];
|
||||
(m.kind.model_key(), format!("entity/parrot/parrot_{name}"))
|
||||
}
|
||||
// AxolotlRenderer.getTextureLocation.
|
||||
S::Axolotl => {
|
||||
let name =
|
||||
["lucy", "wild", "gold", "cyan", "blue"][usize::from(m.extra.variant).min(4)];
|
||||
(
|
||||
baby_or(m, "axolotl#main", "axolotl_baby#main"),
|
||||
b(&format!("entity/axolotl/axolotl_{name}")),
|
||||
)
|
||||
}
|
||||
// SalmonRenderer.submit: the model of the salmon's size.
|
||||
S::Salmon => {
|
||||
let model = match m.extra.variant {
|
||||
0 => "salmon_small#main",
|
||||
2 => "salmon_large#main",
|
||||
_ => "salmon#main",
|
||||
};
|
||||
(model.into(), "entity/fish/salmon".into())
|
||||
}
|
||||
// The baby models and textures of AgeableMobRenderer's other
|
||||
// subclasses.
|
||||
S::Goat => (
|
||||
baby_or(m, "goat#main", "goat_baby#main"),
|
||||
b(&m.kind.texture),
|
||||
),
|
||||
S::PolarBear => (
|
||||
baby_or(m, "polar_bear#main", "polar_bear_baby#main"),
|
||||
b(&m.kind.texture),
|
||||
),
|
||||
S::Ocelot => (
|
||||
baby_or(m, "ocelot#main", "ocelot_baby#main"),
|
||||
b(&m.kind.texture),
|
||||
),
|
||||
S::Turtle => (
|
||||
baby_or(m, "turtle#main", "turtle_baby#main"),
|
||||
b(&m.kind.texture),
|
||||
),
|
||||
S::Armadillo => (
|
||||
baby_or(m, "armadillo#main", "armadillo_baby#main"),
|
||||
b(&m.kind.texture),
|
||||
),
|
||||
// FoxRenderer.getTextureLocation: snow variant, sleeping, baby.
|
||||
S::Fox => (
|
||||
baby_or(m, "fox#main", "fox_baby#main"),
|
||||
format!(
|
||||
"entity/fox/fox{}{}{}",
|
||||
if m.extra.variant == 0 { "" } else { "_snow" },
|
||||
if super::fox::flag(m, super::fox::SLEEPING) {
|
||||
"_sleep"
|
||||
} else {
|
||||
""
|
||||
},
|
||||
if baby { "_baby" } else { "" },
|
||||
),
|
||||
),
|
||||
S::Pig => {
|
||||
let model = if baby {
|
||||
"pig_baby#main".into()
|
||||
@@ -241,7 +322,7 @@ pub fn body(m: &Mob) -> (String, String) {
|
||||
),
|
||||
S::ZombieVillager => (
|
||||
baby_or(m, "zombie_villager#main", "zombie_villager_baby#main"),
|
||||
"entity/zombie_villager/zombie_villager".into(),
|
||||
b("entity/zombie_villager/zombie_villager"),
|
||||
),
|
||||
S::Villager => (
|
||||
baby_or(m, "villager#main", "villager_baby#main"),
|
||||
@@ -601,14 +682,6 @@ pub fn layers(m: &Mob) -> Vec<Layer> {
|
||||
white,
|
||||
false,
|
||||
),
|
||||
// EnergySwirlLayer (the scroll is not reproduced: atlas sprites do not wrap).
|
||||
S::Creeper if m.extra.charged => push(
|
||||
&mut v,
|
||||
"creeper#armor",
|
||||
"entity/creeper/creeper_armor",
|
||||
0x80FF_FFFF,
|
||||
true,
|
||||
),
|
||||
S::TropicalFish => {
|
||||
// TropicalFishPatternLayer: the pattern tinted with the pattern colour.
|
||||
let (model, letter, n) = if m.extra.variant < 6 {
|
||||
@@ -765,7 +838,14 @@ pub fn base_transform_at(m: &Mob, alpha: f32, origin: glam::Mat4) -> glam::Mat4
|
||||
let rotation = match sleep {
|
||||
// setupRotations for Pose.SLEEPING, with the head moved toward the pillow.
|
||||
Some((angle, step)) => {
|
||||
let head = (m.eye_y() - m.position.y) as f32 - 0.1;
|
||||
// The eye height of the standing pose (LivingEntityRenderer
|
||||
// extracts `getEyeHeight(Pose.STANDING)` for a bed).
|
||||
let eye = if avatar(m) {
|
||||
m.kind.eye_height as f32
|
||||
} else {
|
||||
(m.eye_y() - m.position.y) as f32
|
||||
};
|
||||
let head = eye - 0.1;
|
||||
glam::Mat4::from_translation(glam::Vec3::new(-step[0] * head, 0.0, -step[1] * head))
|
||||
* glam::Mat4::from_rotation_y(angle.to_radians())
|
||||
* glam::Mat4::from_rotation_z(90.0_f32.to_radians())
|
||||
@@ -833,10 +913,15 @@ pub fn base_transform_at(m: &Mob, alpha: f32, origin: glam::Mat4) -> glam::Mat4
|
||||
base *= species_scale(m, alpha);
|
||||
base * glam::Mat4::from_translation(glam::Vec3::new(0.0, -1.501, 0.0))
|
||||
}
|
||||
/// `LivingEntityRenderer.isShaking`: converting zombies, piglins and
|
||||
/// hoglins, and a zombie villager being cured.
|
||||
/// `LivingEntityRenderer.isShaking`: a fully frozen mob (the skeleton's own
|
||||
/// `isShaking` is its stray conversion only), converting zombies, piglins
|
||||
/// and hoglins, a zombie villager being cured and a cold strider.
|
||||
fn shaking(m: &Mob) -> bool {
|
||||
if m.is_fully_frozen() && m.species() != Species::Skeleton {
|
||||
return true;
|
||||
}
|
||||
match m.species() {
|
||||
Species::Strider => super::strider::suffocating(m),
|
||||
Species::ZombieVillager => super::zombie_villager::is_converting(m),
|
||||
Species::Zombie | Species::Husk => m.extra.counter2 > 0,
|
||||
Species::Piglin | Species::PiglinBrute | Species::Hoglin => {
|
||||
@@ -854,7 +939,13 @@ fn flip_degrees(m: &Mob) -> f32 {
|
||||
fn species_rotations(m: &Mob, alpha: f32) -> glam::Mat4 {
|
||||
match m.species() {
|
||||
Species::Cat => super::tame::cat_rotations(m, alpha),
|
||||
Species::Panda => super::panda::rotations(m, alpha),
|
||||
Species::Shulker => super::shulker::rotations(m),
|
||||
// FoxRenderer.setupRotations: the body follows a pounce's pitch.
|
||||
Species::Fox if super::fox::flag(m, super::fox::POUNCING | super::fox::FACEPLANTED) => {
|
||||
let pitch = m.previous_head_pitch + (m.head_pitch - m.previous_head_pitch) * alpha;
|
||||
glam::Mat4::from_rotation_x((-pitch).to_radians())
|
||||
}
|
||||
_ => glam::Mat4::IDENTITY,
|
||||
}
|
||||
}
|
||||
@@ -1151,10 +1242,50 @@ fn render_visible(
|
||||
}
|
||||
}
|
||||
}
|
||||
// EnergySwirlLayer: the armour model, additive, scrolling over the sprite.
|
||||
if let Some((model, texture, x_offset)) = energy_swirl(m, s.age)
|
||||
&& let Some(part) = m.kind.layer(model)
|
||||
&& let Some([u0, v0, u1, v1]) = atlas(texture)
|
||||
{
|
||||
layer_poses.reset(part);
|
||||
anim::setup(m, &s, &mut layer_poses, &data.animations);
|
||||
// `RenderTypes.energySwirl`'s texture offset, as the sprite-local UV.
|
||||
let (du, dv) = (x_offset % 1.0, s.age * 0.01 % 1.0);
|
||||
let start = out.len();
|
||||
draw(
|
||||
part,
|
||||
base,
|
||||
&layer_poses,
|
||||
[du, dv, 1.0 + du, 1.0 + dv],
|
||||
crate::breaking::stage_light([u1 - u0, v1 - v0]),
|
||||
crate::breaking::stage_light([u0, v0]),
|
||||
crate::gfx::ENERGY_SWIRL_OVERLAY,
|
||||
out,
|
||||
);
|
||||
// No lighting: the colour bytes carry the sprite's size (fs_energy_swirl).
|
||||
for vertex in &mut out[start..] {
|
||||
vertex.normal = [0; 4];
|
||||
}
|
||||
}
|
||||
if shell_last {
|
||||
draw_body(out);
|
||||
}
|
||||
}
|
||||
/// `EnergySwirlLayer`'s model, texture and `xOffset(t)` for a charged creeper
|
||||
/// (`CreeperPowerLayer`) or a powered wither (`WitherArmorLayer`); `t` is `ageInTicks`.
|
||||
fn energy_swirl(m: &Mob, t: f32) -> Option<(&'static str, &'static str, f32)> {
|
||||
match m.species() {
|
||||
Species::Creeper if m.extra.charged => {
|
||||
Some(("creeper#armor", "entity/creeper/creeper_armor", t * 0.01))
|
||||
}
|
||||
Species::Wither if super::wither::powered(m) => Some((
|
||||
"wither#armor",
|
||||
"entity/wither/wither_armor",
|
||||
(t * 0.02).mcos() * 3.0,
|
||||
)),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
/// The equipment slot of a `HumanoidArmorLayer` model (`zombie#helmet`).
|
||||
fn armor_slot(model: &str) -> Option<usize> {
|
||||
Some(match model.rsplit('#').next()? {
|
||||
@@ -1235,6 +1366,15 @@ fn hand_base(
|
||||
0.0
|
||||
};
|
||||
let mut t = part_matrix(model, base, poses, arm, offset)?;
|
||||
if m.species() == Species::Vex {
|
||||
// `VexModel.translateToHand`: the held item is drawn at 0.55 scale.
|
||||
t *= glam::Mat4::from_scale(glam::Vec3::splat(0.55));
|
||||
t *= glam::Mat4::from_translation(glam::Vec3::new(
|
||||
if left { -0.046_875 } else { 0.046_875 },
|
||||
-0.156_25,
|
||||
0.078_125,
|
||||
));
|
||||
}
|
||||
let (ox, oy, oz) = if m.is_baby() {
|
||||
(0.0, 1.0, -4.5)
|
||||
} else {
|
||||
@@ -1279,6 +1419,12 @@ pub fn held_item_transform(
|
||||
data: &MobData,
|
||||
left: bool,
|
||||
) -> Option<glam::Mat4> {
|
||||
// PandaHoldsItemLayer: the main hand's item in its mouth.
|
||||
if m.species() == Species::Panda {
|
||||
return super::panda::item_offset(m, alpha)
|
||||
.filter(|_| !left)
|
||||
.map(|offset| base_transform(m, alpha, camera) * offset);
|
||||
}
|
||||
if !super::illager::shows_item(m) {
|
||||
return None;
|
||||
}
|
||||
|
||||
@@ -143,6 +143,10 @@ pub fn tick(m: &mut Mob, w: &MobWorld, events: &mut Vec<MobEvent>) {
|
||||
}
|
||||
}
|
||||
}
|
||||
peek_step(m);
|
||||
}
|
||||
/// `Shulker.updatePeekAmount`: the lid eases to the raw peek.
|
||||
fn peek_step(m: &mut Mob) {
|
||||
let a = &mut m.anim;
|
||||
let target = m.extra.counter as f32 * 0.01;
|
||||
let cur = a.v[PEEK];
|
||||
@@ -152,6 +156,13 @@ pub fn tick(m: &mut Mob, w: &MobWorld, events: &mut Vec<MobEvent>) {
|
||||
a.v[PEEK] = (cur + 0.05).clamp(0.0, target);
|
||||
}
|
||||
}
|
||||
/// `Shulker.tick` on a client that is not the server: the lid eases to the
|
||||
/// peek it was sent, and its body stays square (`ShulkerBodyRotationControl`).
|
||||
pub fn client_tick(m: &mut Mob) {
|
||||
m.anim.vo = m.anim.v;
|
||||
peek_step(m);
|
||||
m.body_yaw = 0.0;
|
||||
}
|
||||
/// `Shulker.hurtServer` before the damage: arrows bounce off a closed shell.
|
||||
pub fn accepts(m: &Mob, source: &Damage) -> bool {
|
||||
!(m.extra.counter == 0 && source.kind == DamageKind::Arrow)
|
||||
|
||||
@@ -74,23 +74,8 @@ pub fn head_block(m: &Mob) -> [i32; 3] {
|
||||
/// the dropped seed and the water malus.
|
||||
pub fn ai_step(m: &mut Mob, events: &mut Vec<MobEvent>) {
|
||||
use super::path::PathType;
|
||||
let age = m.age as f32;
|
||||
let current = state(m);
|
||||
if current != m.extra.scratch[0] as u8 {
|
||||
// resetAnimations, then the state's own.
|
||||
for s in &mut m.anim.states[..5] {
|
||||
s.stop();
|
||||
}
|
||||
match current {
|
||||
FEELING_HAPPY => m.anim.states[0].start_if_stopped(age),
|
||||
SCENTING => m.anim.states[1].start_if_stopped(age),
|
||||
SNIFFING => m.anim.states[2].start_if_stopped(age),
|
||||
DIGGING => m.anim.states[3].start_if_stopped(age),
|
||||
RISING => m.anim.states[4].start_if_stopped(age),
|
||||
_ => {}
|
||||
}
|
||||
m.extra.scratch[0] = i32::from(current);
|
||||
}
|
||||
animate(m);
|
||||
if current == DIGGING && now(m) == m.extra.ints[3] {
|
||||
let h = head_block(m);
|
||||
events.push(MobEvent::GiftLoot {
|
||||
@@ -110,6 +95,27 @@ pub fn ai_step(m: &mut Mob, events: &mut Vec<MobEvent>) {
|
||||
let wet = m.fluids.in_water || m.fire_ticks > 0;
|
||||
m.set_malus(PathType::Water, if wet { 0.0 } else { -1.0 });
|
||||
}
|
||||
/// `Sniffer.onSyncedDataUpdated(DATA_STATE)`: the animation states of the
|
||||
/// state it changed to (on a client, each tick for the state it has).
|
||||
pub fn animate(m: &mut Mob) {
|
||||
let age = m.age as f32;
|
||||
let current = state(m);
|
||||
if current != m.extra.scratch[0] as u8 {
|
||||
// resetAnimations, then the state's own.
|
||||
for s in &mut m.anim.states[..5] {
|
||||
s.stop();
|
||||
}
|
||||
match current {
|
||||
FEELING_HAPPY => m.anim.states[0].start_if_stopped(age),
|
||||
SCENTING => m.anim.states[1].start_if_stopped(age),
|
||||
SNIFFING => m.anim.states[2].start_if_stopped(age),
|
||||
DIGGING => m.anim.states[3].start_if_stopped(age),
|
||||
RISING => m.anim.states[4].start_if_stopped(age),
|
||||
_ => {}
|
||||
}
|
||||
m.extra.scratch[0] = i32::from(current);
|
||||
}
|
||||
}
|
||||
/// The brain memory holding the explored positions
|
||||
/// (`SNIFFER_EXPLORED_POSITIONS`).
|
||||
pub fn to_nbt(m: &Mob, map: &mut horizon_world::nbt::Compound) {
|
||||
|
||||
@@ -1500,7 +1500,7 @@ impl Def {
|
||||
),
|
||||
// Nautilus.handleAirSupply: it dries out on land.
|
||||
needs_water: aquatic || s == S::Nautilus,
|
||||
water_sensitive: matches!(s, S::Enderman | S::Blaze | S::SnowGolem),
|
||||
water_sensitive: matches!(s, S::Enderman | S::Blaze | S::SnowGolem | S::Strider),
|
||||
animal: !matches!(s, S::Bat | S::SulfurCube | S::Mannequin)
|
||||
&& !self.hostile_class()
|
||||
&& !aquatic
|
||||
@@ -1629,6 +1629,7 @@ impl Def {
|
||||
match self.species {
|
||||
Species::Dolphin | Species::Axolotl => 1.0,
|
||||
Species::Slime | Species::MagmaCube | Species::SulfurCube => 0.0,
|
||||
Species::Guardian | Species::ElderGuardian | Species::Shulker => 180.0,
|
||||
_ => 40.0,
|
||||
}
|
||||
}
|
||||
@@ -1640,6 +1641,9 @@ impl Def {
|
||||
Species::Sniffer => 50.0,
|
||||
Species::Goat => 15.0,
|
||||
Species::Armadillo => 32.0,
|
||||
Species::Ravager => 45.0,
|
||||
Species::Camel | Species::CamelHusk => 30.0,
|
||||
Species::Shulker => 180.0,
|
||||
_ => 75.0,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -297,6 +297,7 @@ pub fn accepts_damage(m: &mut Mob, source: &Damage, amount: f32) -> bool {
|
||||
| DamageKind::Fall
|
||||
| DamageKind::Sting
|
||||
| DamageKind::Cactus
|
||||
| DamageKind::Freeze
|
||||
);
|
||||
if immune {
|
||||
if let Some(from) = source.from {
|
||||
|
||||
Loaded 100 of 137 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user