feat: add pillager raids

This commit is contained in:
Alexander Medvedev
2026-08-24 13:24:47 +02:00
parent 4f7a517ce1
commit 97fdea50d2
49 changed files with 6318 additions and 470 deletions

View File

@@ -1,85 +1,216 @@
use std::io::{Error, Write};
use crate::{
codec::{var_int::VarInt, var_long::VarLong},
serial::PacketWrite,
};
use pumpkin_macros::packet;
use crate::{
codec::var_long::VarLong,
serial::{PacketRead, PacketWrite},
};
pub const BOSS_EVENT_SHOW: u8 = 0;
pub const BOSS_EVENT_REGISTER_PLAYER: u8 = 1;
pub const BOSS_EVENT_HIDE: u8 = 2;
pub const BOSS_EVENT_UNREGISTER_PLAYER: u8 = 3;
pub const BOSS_EVENT_HEALTH_PERCENTAGE: u8 = 4;
pub const BOSS_EVENT_TITLE: u8 = 5;
pub const BOSS_EVENT_APPEARANCE_PROPERTIES: u8 = 6;
pub const BOSS_EVENT_TEXTURE: u8 = 7;
pub const BOSS_EVENT_REQUEST: u8 = 8;
pub const BOSS_EVENT_COLOUR_PINK: u8 = 0;
pub const BOSS_EVENT_COLOUR_BLUE: u8 = 1;
pub const BOSS_EVENT_COLOUR_RED: u8 = 2;
pub const BOSS_EVENT_COLOUR_GREEN: u8 = 3;
pub const BOSS_EVENT_COLOUR_YELLOW: u8 = 4;
pub const BOSS_EVENT_COLOUR_PURPLE: u8 = 5;
pub const BOSS_EVENT_COLOUR_REBECCA_PURPLE: u8 = 6;
pub const BOSS_EVENT_COLOUR_WHITE: u8 = 7;
pub const BOSS_EVENT_OVERLAY_PROGRESS: u8 = 0;
pub const BOSS_EVENT_OVERLAY_NOTCHED_6: u8 = 1;
pub const BOSS_EVENT_OVERLAY_NOTCHED_10: u8 = 2;
pub const BOSS_EVENT_OVERLAY_NOTCHED_12: u8 = 3;
pub const BOSS_EVENT_OVERLAY_NOTCHED_20: u8 = 4;
/// Sent by the server to make a specific 'boss event' occur in the world.
///
/// Packet ID: `74`
#[derive(PacketWrite, PacketRead, Clone, Debug, PartialEq)]
#[packet(74)]
pub struct CBossEvent {
/// The unique ID of the boss entity that the boss event sent involves.
pub boss_entity_id: VarLong,
pub action: BossEventAction,
/// The unique ID of the player that is registered to or unregistered from the boss fight.
pub player_entity_id: VarLong,
/// The type of the event (one of `BOSS_EVENT_*`).
pub event_type: u8,
/// The title shown above the boss bar.
pub title: String,
/// Filtered version of `title` with profanity removed.
pub filtered_title: String,
/// The percentage of health shown in the boss bar (0.0 - 1.0).
pub health_percentage: f32,
/// The colour of the boss bar (one of `BOSS_EVENT_COLOUR_*`).
pub color: u8,
/// The overlay of the boss bar (one of `BOSS_EVENT_OVERLAY_*`).
pub overlay: u8,
}
pub enum BossEventAction {
Add {
impl CBossEvent {
#[allow(clippy::too_many_arguments)]
#[must_use]
pub const fn new(
boss_entity_id: VarLong,
player_entity_id: VarLong,
event_type: u8,
title: String,
health_percent: f32,
color: VarInt,
overlay: VarInt,
},
Remove,
UpdateHealth(f32),
UpdateTitle(String),
UpdateProperties {
color: VarInt,
overlay: VarInt,
},
}
impl PacketWrite for CBossEvent {
fn write<W: Write>(&self, writer: &mut W) -> Result<(), Error> {
self.boss_entity_id.write(writer)?;
VarLong(0).write(writer)?; // player_entity_id
let event_type: u8;
let mut title = String::new();
let mut health_percent = 0.0f32;
let mut color: u8 = 0;
let mut overlay: u8 = 0;
match &self.action {
BossEventAction::Add {
title: t,
health_percent: hp,
color: c,
overlay: o,
} => {
event_type = 0;
title.clone_from(t);
health_percent = *hp;
color = c.0 as u8;
overlay = o.0 as u8;
}
BossEventAction::Remove => {
event_type = 2;
}
BossEventAction::UpdateHealth(health) => {
event_type = 3;
health_percent = *health;
}
BossEventAction::UpdateTitle(t) => {
event_type = 4;
title.clone_from(t);
}
BossEventAction::UpdateProperties {
color: c,
overlay: o,
} => {
event_type = 5;
color = c.0 as u8;
overlay = o.0 as u8;
}
filtered_title: String,
health_percentage: f32,
color: u8,
overlay: u8,
) -> Self {
Self {
boss_entity_id,
player_entity_id,
event_type,
title,
filtered_title,
health_percentage,
color,
overlay,
}
}
event_type.write(writer)?;
title.write(writer)?; // title
title.write(writer)?; // filtered_title
health_percent.write(writer)?;
color.write(writer)?;
overlay.write(writer)?;
#[must_use]
pub fn show(
boss_entity_id: VarLong,
player_entity_id: VarLong,
title: impl Into<String>,
health_percentage: f32,
color: u8,
overlay: u8,
) -> Self {
let title = title.into();
Self {
boss_entity_id,
player_entity_id,
event_type: BOSS_EVENT_SHOW,
filtered_title: title.clone(),
title,
health_percentage,
color,
overlay,
}
}
Ok(())
#[must_use]
pub const fn register_player(boss_entity_id: VarLong, player_entity_id: VarLong) -> Self {
Self {
boss_entity_id,
player_entity_id,
event_type: BOSS_EVENT_REGISTER_PLAYER,
title: String::new(),
filtered_title: String::new(),
health_percentage: 0.0,
color: 0,
overlay: 0,
}
}
#[must_use]
pub const fn hide(boss_entity_id: VarLong) -> Self {
Self {
boss_entity_id,
player_entity_id: VarLong(0),
event_type: BOSS_EVENT_HIDE,
title: String::new(),
filtered_title: String::new(),
health_percentage: 0.0,
color: 0,
overlay: 0,
}
}
#[must_use]
pub const fn unregister_player(boss_entity_id: VarLong, player_entity_id: VarLong) -> Self {
Self {
boss_entity_id,
player_entity_id,
event_type: BOSS_EVENT_UNREGISTER_PLAYER,
title: String::new(),
filtered_title: String::new(),
health_percentage: 0.0,
color: 0,
overlay: 0,
}
}
#[must_use]
pub const fn update_health(boss_entity_id: VarLong, health_percentage: f32) -> Self {
Self {
boss_entity_id,
player_entity_id: VarLong(0),
event_type: BOSS_EVENT_HEALTH_PERCENTAGE,
title: String::new(),
filtered_title: String::new(),
health_percentage,
color: 0,
overlay: 0,
}
}
#[must_use]
pub fn update_title(boss_entity_id: VarLong, title: impl Into<String>) -> Self {
let title = title.into();
Self {
boss_entity_id,
player_entity_id: VarLong(0),
event_type: BOSS_EVENT_TITLE,
filtered_title: title.clone(),
title,
health_percentage: 0.0,
color: 0,
overlay: 0,
}
}
#[must_use]
pub const fn update_properties(boss_entity_id: VarLong, color: u8, overlay: u8) -> Self {
Self {
boss_entity_id,
player_entity_id: VarLong(0),
event_type: BOSS_EVENT_APPEARANCE_PROPERTIES,
title: String::new(),
filtered_title: String::new(),
health_percentage: 0.0,
color,
overlay,
}
}
#[must_use]
pub const fn update_texture(boss_entity_id: VarLong, color: u8, overlay: u8) -> Self {
Self {
boss_entity_id,
player_entity_id: VarLong(0),
event_type: BOSS_EVENT_TEXTURE,
title: String::new(),
filtered_title: String::new(),
health_percentage: 0.0,
color,
overlay,
}
}
#[must_use]
pub const fn request(boss_entity_id: VarLong, player_entity_id: VarLong) -> Self {
Self {
boss_entity_id,
player_entity_id,
event_type: BOSS_EVENT_REQUEST,
title: String::new(),
filtered_title: String::new(),
health_percentage: 0.0,
color: 0,
overlay: 0,
}
}
}

View File

@@ -88,6 +88,7 @@ impl BlockBehaviour for WitherSkeletonSkullBlock {
&EntityType::WITHER,
);
let wither = WitherEntity::new(entity);
wither.make_invulnerable();
world.spawn_entity(wither).await;
return;
}

View File

@@ -125,7 +125,7 @@ pub enum Arg<'a> {
CommandTree(CommandTree),
Item(&'a str),
ItemPredicate(&'a str),
ResourceLocation(&'a str),
ResourceLocation(pumpkin_util::identifier::Identifier),
Block(&'a str),
BlockPredicate(&'a str),
BossbarColor(BossbarColor),

View File

@@ -6,6 +6,7 @@ use crate::command::dispatcher::CommandError;
use crate::command::tree::RawArgs;
use crate::server::Server;
use pumpkin_protocol::java::client::play::{ArgumentType, SuggestionProviders};
use pumpkin_util::identifier::Identifier;
// TODO: Add proper autocomplete
pub struct ResourceLocationArgumentConsumer;
@@ -21,39 +22,16 @@ impl GetClientSideArgParser for ResourceLocationArgumentConsumer {
}
impl ArgumentConsumer for ResourceLocationArgumentConsumer {
fn consume<'a, 'b>(
fn consume<'a>(
&'a self,
_sender: &'a CommandSender,
_server: &'a Server,
args: &'b mut RawArgs<'a>,
args: &mut RawArgs<'a>,
) -> ConsumeResult<'a> {
let s_opt: Option<&'a str> = args.pop().map(|arg| arg.value);
let identifier = args.pop().and_then(|arg| Identifier::parse(arg.value).ok());
Box::pin(async move { s_opt.map(Arg::ResourceLocation) })
Box::pin(async move { identifier.map(Arg::ResourceLocation) })
}
// async fn suggest<'a>(
// &'a self,
// _sender: &CommandSender,
// _server: &'a Server,
// _input: &'a str,
// ) -> Result<Option<Vec<CommandSuggestion>>, CommandError> {
// if !self.autocomplete {
// return Ok(None);
// }
// // TODO
// // let suggestions = server
// // .bossbars
// // .lock()
// // .await
// // .custom_bossbars
// // .keys()
// // .map(|suggestion| CommandSuggestion::new(suggestion, None))
// // .collect();
// Ok(None)
// }
}
impl DefaultNameArgConsumer for ResourceLocationArgumentConsumer {
@@ -63,7 +41,7 @@ impl DefaultNameArgConsumer for ResourceLocationArgumentConsumer {
}
impl<'a> FindArg<'a> for ResourceLocationArgumentConsumer {
type Data = &'a str;
type Data = &'a Identifier;
fn find_arg(args: &'a super::ConsumedArgs, name: &str) -> Result<Self::Data, CommandError> {
match args.get(name) {

View File

@@ -9,9 +9,11 @@ use crate::command::args::{ConsumedArgs, FindArg, FindArgDefaultName};
use crate::command::args::textcomponent::TextComponentArgConsumer;
use crate::command::dispatcher::CommandError;
use crate::command::tree::CommandTree;
use crate::command::suggestion::suggestions::SuggestionsBuilder;
use crate::command::tree::builder::{argument, argument_default_name, literal};
use crate::command::tree::{CommandSuggestionProvider, CommandSuggestionResult, CommandTree};
use crate::command::{CommandExecutor, CommandResult, CommandSender};
use crate::server::Server;
use crate::world::bossbar::Bossbar;
use crate::world::custom_bossbar::BossbarUpdateError;
use pumpkin_data::translation;
@@ -20,17 +22,41 @@ use pumpkin_util::text::hover::HoverEvent;
use uuid::Uuid;
const NAMES: [&str; 1] = ["bossbar"];
const DESCRIPTION: &str = "Display bossbar";
const DESCRIPTION: &str = "Creates and modifies boss bars";
const ARG_NAME: &str = "name";
const ARG_VISIBLE: &str = "visible";
const fn autocomplete_consumer() -> ResourceLocationArgumentConsumer {
// TODO: Add autocompletion when implemented properly
ResourceLocationArgumentConsumer
}
struct BossbarSuggestionProvider;
impl CommandSuggestionProvider for BossbarSuggestionProvider {
fn suggest<'a>(
&'a self,
_src: &'a CommandSender,
server: &'a Server,
input: &'a str,
start: usize,
_end: usize,
) -> CommandSuggestionResult<'a> {
Box::pin(async move {
let mut builder = SuggestionsBuilder::new(input, start);
let bossbars = server.bossbars.lock().await;
let remaining = builder.remaining_lowercase().to_string();
for key in bossbars.custom_bossbars.keys() {
if key.to_lowercase().starts_with(&remaining) {
builder = builder.suggest(key.clone());
}
}
builder.build()
})
}
}
enum CommandValueGet {
Max,
Players,
@@ -58,12 +84,9 @@ impl CommandExecutor for AddExecutor {
args: &'a ConsumedArgs<'a>,
) -> CommandResult<'a> {
Box::pin(async move {
let mut namespace = autocomplete_consumer()
let namespace = autocomplete_consumer()
.find_arg_default_name(args)?
.to_string();
if !namespace.contains(':') {
namespace = format!("minecraft:{namespace}");
}
let text_component = TextComponentArgConsumer::find_arg(args, ARG_NAME)?;
@@ -98,6 +121,7 @@ impl CommandExecutor for AddExecutor {
struct GetExecutor(CommandValueGet);
impl CommandExecutor for GetExecutor {
#[expect(clippy::too_many_lines)]
fn execute<'a>(
&'a self,
sender: &'a CommandSender,
@@ -132,7 +156,48 @@ impl CommandExecutor for GetExecutor {
.await;
Ok(bossbar.max)
}
CommandValueGet::Players => Ok(bossbar.players.len() as i32),
CommandValueGet::Players => {
let online_players: Vec<String> = server
.get_all_players()
.iter()
.filter(|player| bossbar.players.contains(&player.gameprofile.id))
.map(|player| player.gameprofile.name.clone())
.collect();
let count = online_players.len() as i32;
if count == 0 {
sender
.send_message(TextComponent::translate_cross(
translation::java::COMMANDS_BOSSBAR_GET_PLAYERS_NONE,
translation::bedrock::COMMANDS_BOSSBAR_GET_PLAYERS_NONE,
[bossbar_prefix(
bossbar.bossbar_data.title.clone(),
namespace.clone(),
)],
))
.await;
} else {
sender
.send_message(TextComponent::translate_cross(
translation::java::COMMANDS_BOSSBAR_GET_PLAYERS_SOME,
if count == 1 {
translation::bedrock::COMMANDS_BOSSBAR_GET_PLAYERS_ONE
} else {
translation::bedrock::COMMANDS_BOSSBAR_GET_PLAYERS
},
[
bossbar_prefix(
bossbar.bossbar_data.title.clone(),
namespace.clone(),
),
TextComponent::text(count.to_string()),
TextComponent::text(online_players.join(", ")),
],
))
.await;
}
Ok(count)
}
CommandValueGet::Value => {
sender
.send_message(TextComponent::translate_cross(
@@ -152,12 +217,12 @@ impl CommandExecutor for GetExecutor {
CommandValueGet::Visible => {
let (java_key, bedrock_key) = if bossbar.visible {
(
translation::java::COMMANDS_BOSSBAR_SET_VISIBLE_SUCCESS_VISIBLE,
translation::java::COMMANDS_BOSSBAR_GET_VISIBLE_VISIBLE,
translation::bedrock::COMMANDS_BOSSBAR_GET_VISIBLE_TRUE,
)
} else {
(
translation::java::COMMANDS_BOSSBAR_SET_VISIBLE_SUCCESS_HIDDEN,
translation::java::COMMANDS_BOSSBAR_GET_VISIBLE_HIDDEN,
translation::bedrock::COMMANDS_BOSSBAR_GET_VISIBLE_FALSE,
)
};
@@ -270,7 +335,7 @@ impl CommandExecutor for RemoveExecutor {
.bossbars
.lock()
.await
.remove_bossbar(server, namespace.clone())
.remove_bossbar(server, namespace)
.await
{
Ok(()) => return Ok(server.bossbars.lock().await.get_bossbars_len() as i32),
@@ -294,11 +359,13 @@ impl CommandExecutor for SetExecutor {
args: &'a ConsumedArgs<'a>,
) -> CommandResult<'a> {
Box::pin(async move {
let namespace = autocomplete_consumer().find_arg_default_name(args)?;
let namespace = autocomplete_consumer()
.find_arg_default_name(args)?
.to_string();
let Some(bossbar) = server.bossbars.lock().await.get_bossbar(namespace) else {
let Some(bossbar) = server.bossbars.lock().await.get_bossbar(&namespace) else {
return Err(handle_bossbar_error(
BossbarUpdateError::InvalidResourceLocation(namespace.to_string()),
BossbarUpdateError::InvalidResourceLocation(namespace),
));
};
@@ -310,7 +377,7 @@ impl CommandExecutor for SetExecutor {
.bossbars
.lock()
.await
.update_color(server, namespace.to_string(), *color)
.update_color(server, namespace.clone(), *color)
.await
{
Ok(()) => {}
@@ -321,11 +388,11 @@ impl CommandExecutor for SetExecutor {
sender
.send_message(TextComponent::translate_cross(
"commands.bossbar.set.color.success",
"commands.bossbar.set.color.success",
translation::java::COMMANDS_BOSSBAR_SET_COLOR_SUCCESS,
translation::java::COMMANDS_BOSSBAR_SET_COLOR_SUCCESS,
[bossbar_prefix(
bossbar.bossbar_data.title.clone(),
namespace.to_string(),
namespace,
)],
))
.await;
@@ -345,7 +412,7 @@ impl CommandExecutor for SetExecutor {
.bossbars
.lock()
.await
.update_health(server, namespace.to_string(), max_value, bossbar.value)
.update_max(server, namespace.clone(), max_value)
.await
{
Ok(()) => {}
@@ -356,13 +423,10 @@ impl CommandExecutor for SetExecutor {
sender
.send_message(TextComponent::translate_cross(
"commands.bossbar.set.max.success",
"commands.bossbar.set.max.success",
translation::java::COMMANDS_BOSSBAR_SET_MAX_SUCCESS,
translation::java::COMMANDS_BOSSBAR_SET_MAX_SUCCESS,
[
bossbar_prefix(
bossbar.bossbar_data.title.clone(),
namespace.to_string(),
),
bossbar_prefix(bossbar.bossbar_data.title.clone(), namespace),
TextComponent::text(max_value.to_string()),
],
))
@@ -376,7 +440,7 @@ impl CommandExecutor for SetExecutor {
.bossbars
.lock()
.await
.update_name(server, namespace, text_component.clone())
.update_name(server, &namespace, text_component.clone())
.await
{
Ok(()) => {}
@@ -387,9 +451,9 @@ impl CommandExecutor for SetExecutor {
sender
.send_message(TextComponent::translate_cross(
"commands.bossbar.set.name.success",
"commands.bossbar.set.name.success",
[bossbar_prefix(text_component, namespace.to_string())],
translation::java::COMMANDS_BOSSBAR_SET_NAME_SUCCESS,
translation::java::COMMANDS_BOSSBAR_SET_NAME_SUCCESS,
[bossbar_prefix(text_component, namespace)],
))
.await;
@@ -401,7 +465,7 @@ impl CommandExecutor for SetExecutor {
.bossbars
.lock()
.await
.update_players(server, namespace.to_string(), vec![])
.update_players(server, namespace.clone(), vec![])
.await
{
Ok(()) => {}
@@ -411,11 +475,11 @@ impl CommandExecutor for SetExecutor {
}
sender
.send_message(TextComponent::translate_cross(
"commands.bossbar.set.players.success.none",
"commands.bossbar.set.players.success.none",
translation::java::COMMANDS_BOSSBAR_SET_PLAYERS_SUCCESS_NONE,
translation::java::COMMANDS_BOSSBAR_SET_PLAYERS_SUCCESS_NONE,
[bossbar_prefix(
bossbar.bossbar_data.title.clone(),
namespace.to_string(),
namespace,
)],
))
.await;
@@ -432,7 +496,7 @@ impl CommandExecutor for SetExecutor {
.bossbars
.lock()
.await
.update_players(server, namespace.to_string(), players)
.update_players(server, namespace.clone(), players)
.await
{
Ok(()) => {}
@@ -449,13 +513,10 @@ impl CommandExecutor for SetExecutor {
sender
.send_message(TextComponent::translate_cross(
"commands.bossbar.set.players.success.some",
"commands.bossbar.set.players.success.some",
translation::java::COMMANDS_BOSSBAR_SET_PLAYERS_SUCCESS_SOME,
translation::java::COMMANDS_BOSSBAR_SET_PLAYERS_SUCCESS_SOME,
[
bossbar_prefix(
bossbar.bossbar_data.title.clone(),
namespace.to_string(),
),
bossbar_prefix(bossbar.bossbar_data.title.clone(), namespace),
TextComponent::text(count.to_string()),
TextComponent::text(player_names),
],
@@ -470,7 +531,7 @@ impl CommandExecutor for SetExecutor {
.bossbars
.lock()
.await
.update_division(server, namespace.to_string(), *style)
.update_division(server, namespace.clone(), *style)
.await
{
Ok(()) => {}
@@ -480,11 +541,11 @@ impl CommandExecutor for SetExecutor {
}
sender
.send_message(TextComponent::translate_cross(
"commands.bossbar.set.style.success",
"commands.bossbar.set.style.success",
translation::java::COMMANDS_BOSSBAR_SET_STYLE_SUCCESS,
translation::java::COMMANDS_BOSSBAR_SET_STYLE_SUCCESS,
[bossbar_prefix(
bossbar.bossbar_data.title.clone(),
namespace.to_string(),
namespace,
)],
))
.await;
@@ -503,7 +564,7 @@ impl CommandExecutor for SetExecutor {
.bossbars
.lock()
.await
.update_health(server, namespace.to_string(), bossbar.max, value)
.update_value(server, namespace.clone(), value)
.await
{
Ok(()) => {}
@@ -514,13 +575,10 @@ impl CommandExecutor for SetExecutor {
sender
.send_message(TextComponent::translate_cross(
"commands.bossbar.set.value.success",
"commands.bossbar.set.value.success",
translation::java::COMMANDS_BOSSBAR_SET_VALUE_SUCCESS,
translation::java::COMMANDS_BOSSBAR_SET_VALUE_SUCCESS,
[
bossbar_prefix(
bossbar.bossbar_data.title.clone(),
namespace.to_string(),
),
bossbar_prefix(bossbar.bossbar_data.title.clone(), namespace),
TextComponent::text(value.to_string()),
],
))
@@ -535,7 +593,7 @@ impl CommandExecutor for SetExecutor {
.bossbars
.lock()
.await
.update_visibility(server, namespace.to_string(), visibility)
.update_visibility(server, namespace.clone(), visibility)
.await
{
Ok(()) => {}
@@ -545,9 +603,9 @@ impl CommandExecutor for SetExecutor {
}
let state = if visibility {
"commands.bossbar.set.visible.success.visible"
translation::java::COMMANDS_BOSSBAR_SET_VISIBLE_SUCCESS_VISIBLE
} else {
"commands.bossbar.set.visible.success.hidden"
translation::java::COMMANDS_BOSSBAR_SET_VISIBLE_SUCCESS_HIDDEN
};
sender
@@ -556,12 +614,12 @@ impl CommandExecutor for SetExecutor {
state,
[bossbar_prefix(
bossbar.bossbar_data.title.clone(),
namespace.to_string(),
namespace,
)],
))
.await;
Ok(visibility as i32)
Ok(0)
}
}
})
@@ -587,6 +645,7 @@ pub fn init_command_tree() -> CommandTree {
.then(
literal("get").then(
argument_default_name(autocomplete_consumer())
.suggests(BossbarSuggestionProvider)
.then(literal("max").execute(GetExecutor(CommandValueGet::Max)))
.then(literal("players").execute(GetExecutor(CommandValueGet::Players)))
.then(literal("value").execute(GetExecutor(CommandValueGet::Value)))
@@ -595,12 +654,16 @@ pub fn init_command_tree() -> CommandTree {
)
.then(literal("list").execute(ListExecutor))
.then(
literal("remove")
.then(argument_default_name(autocomplete_consumer()).execute(RemoveExecutor)),
literal("remove").then(
argument_default_name(autocomplete_consumer())
.suggests(BossbarSuggestionProvider)
.execute(RemoveExecutor),
),
)
.then(
literal("set").then(
argument_default_name(autocomplete_consumer())
.suggests(BossbarSuggestionProvider)
.then(
literal("color").then(
argument_default_name(BossbarColorArgumentConsumer)

View File

@@ -53,6 +53,7 @@ mod playsound;
mod plugin;
mod plugins;
mod pumpkin;
mod raid;
mod random;
mod recipe;
mod reload;
@@ -157,6 +158,7 @@ pub fn default_dispatcher(
dispatcher.register(spectate::init_command_tree(), "minecraft:command.spectate");
dispatcher.register(data::init_command_tree(), "minecraft:command.data");
dispatcher.register(waypoint::init_command_tree(), "minecraft:command.waypoint");
dispatcher.register(raid::init_command_tree(), "minecraft:command.raid");
// Three
dispatcher.register(deop::init_command_tree(), "minecraft:command.deop");
dispatcher.register(kick::init_command_tree(), "minecraft:command.kick");

View File

@@ -0,0 +1,339 @@
use uuid::Uuid;
use pumpkin_data::data_component_impl::EquipmentSlot;
use pumpkin_data::effect::StatusEffect;
use pumpkin_data::entity::EntityType;
use pumpkin_data::potion::Effect;
use pumpkin_data::sound::Sound;
use pumpkin_util::text::TextComponent;
use crate::command::args::bounded_num::BoundedNumArgumentConsumer;
use crate::command::args::{ConsumedArgs, FindArg};
use crate::command::dispatcher::CommandError;
use crate::command::tree::CommandTree;
use crate::command::tree::builder::{argument, literal};
use crate::command::{CommandExecutor, CommandResult, CommandSender};
use crate::entity::EntityBase;
use crate::entity::mob::raider::create_ominous_banner;
use crate::entity::r#type::from_type;
use crate::server::Server;
const NAMES: [&str; 1] = ["raid"];
const DESCRIPTION: &str = "Controls or queries village raids.";
const ARG_OMEN_LVL: &str = "omenlvl";
const ARG_LEVEL: &str = "level";
struct StartExecutor {
has_omen_lvl: bool,
}
impl CommandExecutor for StartExecutor {
fn execute<'a>(
&'a self,
sender: &'a CommandSender,
_server: &'a Server,
args: &'a ConsumedArgs<'a>,
) -> CommandResult<'a> {
Box::pin(async move {
let player = sender.as_player().ok_or(CommandError::InvalidRequirement)?;
let entity = player.get_entity();
let pos = entity.block_pos.load();
let world = entity.world.load();
let mut raids = world.raids.lock().await;
if raids.get_raid_at(&pos).is_some() {
sender
.send_message(TextComponent::text("Raid already started close by"))
.await;
return Ok(0);
}
let omen_lvl = if self.has_omen_lvl {
BoundedNumArgumentConsumer::<i32>::find_arg(args, ARG_OMEN_LVL)
.ok()
.and_then(Result::ok)
.unwrap_or(1)
} else {
1
};
let raid_id = raids.create_or_extend_raid(&player, pos, &world);
if let Some(id) = raid_id {
if let Some(raid) = raids.get_mut(id) {
raid.set_raid_omen_level(omen_lvl);
}
sender
.send_message(TextComponent::text("Created a raid in your local village"))
.await;
Ok(1)
} else {
sender
.send_message(TextComponent::text(
"Failed to create a raid in your local village",
))
.await;
Ok(0)
}
})
}
}
struct StopExecutor;
impl CommandExecutor for StopExecutor {
fn execute<'a>(
&'a self,
sender: &'a CommandSender,
_server: &'a Server,
_args: &'a ConsumedArgs<'a>,
) -> CommandResult<'a> {
Box::pin(async move {
let player = sender.as_player().ok_or(CommandError::InvalidRequirement)?;
let entity = player.get_entity();
let pos = entity.block_pos.load();
let world = entity.world.load();
let mut raids = world.raids.lock().await;
if let Some(raid) = raids.get_raid_at_mut(&pos) {
raid.stop(&world).await;
sender
.send_message(TextComponent::text("Stopped raid"))
.await;
Ok(1)
} else {
sender
.send_message(TextComponent::text("No raid here"))
.await;
Ok(0)
}
})
}
}
struct CheckExecutor;
impl CommandExecutor for CheckExecutor {
fn execute<'a>(
&'a self,
sender: &'a CommandSender,
_server: &'a Server,
_args: &'a ConsumedArgs<'a>,
) -> CommandResult<'a> {
Box::pin(async move {
let player = sender.as_player().ok_or(CommandError::InvalidRequirement)?;
let entity = player.get_entity();
let pos = entity.block_pos.load();
let world = entity.world.load();
let raids = world.raids.lock().await;
if let Some(raid) = raids.get_raid_at(&pos) {
sender
.send_message(TextComponent::text("Found a started raid!"))
.await;
let alive = raid.get_total_raiders_alive();
let living_health = raid.get_health_of_living_raiders(&world);
let msg = format!(
"Num groups spawned: {} Raid omen level: {} Num mobs: {} Raid health: {} / {}",
raid.get_groups_spawned(),
raid.get_raid_omen_level(),
alive,
living_health,
raid.total_health
);
sender.send_message(TextComponent::text(msg)).await;
Ok(1)
} else {
sender
.send_message(TextComponent::text("Found no started raids"))
.await;
Ok(0)
}
})
}
}
struct SoundExecutor;
impl CommandExecutor for SoundExecutor {
fn execute<'a>(
&'a self,
sender: &'a CommandSender,
_server: &'a Server,
_args: &'a ConsumedArgs<'a>,
) -> CommandResult<'a> {
Box::pin(async move {
let player = sender.as_player().ok_or(CommandError::InvalidRequirement)?;
let entity = player.get_entity();
let pos = entity.pos.load();
let world = entity.world.load();
let sound_pos = pos.add_raw(5.0, 0.0, 0.0);
world.play_sound(
Sound::EventRaidHorn,
pumpkin_data::sound::SoundCategory::Neutral,
&sound_pos,
);
Ok(1)
})
}
}
struct SpawnLeaderExecutor;
impl CommandExecutor for SpawnLeaderExecutor {
fn execute<'a>(
&'a self,
sender: &'a CommandSender,
_server: &'a Server,
_args: &'a ConsumedArgs<'a>,
) -> CommandResult<'a> {
Box::pin(async move {
let player = sender.as_player().ok_or(CommandError::InvalidRequirement)?;
let entity = player.get_entity();
let pos = entity.pos.load();
let world = entity.world.load();
let raider_uuid = Uuid::new_v4();
let raider_entity = from_type(&EntityType::PILLAGER, pos, &world, raider_uuid);
if let Some(mob) = raider_entity.get_mob()
&& let Some(raider) = mob.as_raider()
{
raider.set_patrol_leader(true);
let banner = create_ominous_banner();
let living = &mob.get_mob_entity().living_entity;
let mut equipment = living.entity_equipment.lock().await;
equipment.put(&EquipmentSlot::HEAD, banner.clone());
drop(equipment);
living.send_equipment_changes(&[(EquipmentSlot::HEAD, banner)]);
}
world.spawn_entity(raider_entity).await;
sender
.send_message(TextComponent::text("Spawned a raid captain"))
.await;
Ok(1)
})
}
}
struct SetOmenExecutor;
impl CommandExecutor for SetOmenExecutor {
fn execute<'a>(
&'a self,
sender: &'a CommandSender,
_server: &'a Server,
args: &'a ConsumedArgs<'a>,
) -> CommandResult<'a> {
Box::pin(async move {
let player = sender.as_player().ok_or(CommandError::InvalidRequirement)?;
let entity = player.get_entity();
let pos = entity.block_pos.load();
let world = entity.world.load();
let level = BoundedNumArgumentConsumer::<i32>::find_arg(args, ARG_LEVEL)
.ok()
.and_then(Result::ok)
.unwrap_or(1);
let mut raids = world.raids.lock().await;
if let Some(raid) = raids.get_raid_at_mut(&pos) {
if level > 5 {
sender
.send_message(TextComponent::text(
"Sorry, the max raid omen level you can set is 5",
))
.await;
return Ok(0);
}
let before = raid.get_raid_omen_level();
raid.set_raid_omen_level(level);
sender
.send_message(TextComponent::text(format!(
"Changed village's raid omen level from {before} to {level}"
)))
.await;
Ok(1)
} else {
sender
.send_message(TextComponent::text("No raid found here"))
.await;
Ok(0)
}
})
}
}
struct GlowExecutor;
impl CommandExecutor for GlowExecutor {
fn execute<'a>(
&'a self,
sender: &'a CommandSender,
_server: &'a Server,
_args: &'a ConsumedArgs<'a>,
) -> CommandResult<'a> {
Box::pin(async move {
let player = sender.as_player().ok_or(CommandError::InvalidRequirement)?;
let entity = player.get_entity();
let pos = entity.block_pos.load();
let world = entity.world.load();
let raids = world.raids.lock().await;
if let Some(raid) = raids.get_raid_at(&pos) {
let effect = Effect {
effect_type: &StatusEffect::GLOWING,
duration: 1000,
amplifier: 1,
ambient: false,
show_particles: false,
show_icon: true,
blend: true,
};
for raider_uuid in raid.get_all_raiders() {
if let Some(e) = world.get_entity_by_uuid(raider_uuid)
&& let Some(living) = e.get_living_entity()
{
living.add_effect(effect.clone()).await;
}
}
Ok(1)
} else {
sender
.send_message(TextComponent::text("No raid found here"))
.await;
Ok(0)
}
})
}
}
pub fn init_command_tree() -> CommandTree {
CommandTree::new(NAMES, DESCRIPTION)
.then(
literal("start")
.execute(StartExecutor {
has_omen_lvl: false,
})
.then(
argument(
ARG_OMEN_LVL,
BoundedNumArgumentConsumer::<i32>::new().min(0),
)
.execute(StartExecutor { has_omen_lvl: true }),
),
)
.then(literal("stop").execute(StopExecutor))
.then(literal("check").execute(CheckExecutor))
.then(literal("sound").execute(SoundExecutor))
.then(literal("spawnleader").execute(SpawnLeaderExecutor))
.then(
literal("setomen").then(
argument(ARG_LEVEL, BoundedNumArgumentConsumer::<i32>::new().min(0))
.execute(SetOmenExecutor),
),
)
.then(literal("glow").execute(GlowExecutor))
}

View File

@@ -69,7 +69,7 @@ impl CommandExecutor for QueryExecutor {
) -> CommandResult<'a> {
Box::pin(async move {
let clock_name = ResourceLocationArgumentConsumer::find_arg(args, ARG_CLOCK)
.unwrap_or(DEFAULT_CLOCK);
.map_or_else(|_| DEFAULT_CLOCK.to_string(), ToString::to_string);
let mode = self.0;
let worlds = server.worlds.load();
let world = worlds.first().ok_or(CommandError::InvalidRequirement)?;
@@ -93,7 +93,7 @@ impl CommandExecutor for QueryExecutor {
.send_message(pumpkin_macros::translate_cross!(
translation::java::COMMANDS_TIME_QUERY_ABSOLUTE,
translation::bedrock::COMMANDS_TIME_QUERY_DAYTIME,
TextComponent::text(clock_name.to_string()),
TextComponent::text(clock_name.clone()),
TextComponent::text(total_ticks.to_string())
))
.await;
@@ -137,7 +137,7 @@ impl CommandExecutor for ActionExecutor {
) -> CommandResult<'a> {
Box::pin(async move {
let clock_name = ResourceLocationArgumentConsumer::find_arg(args, ARG_CLOCK)
.unwrap_or(DEFAULT_CLOCK);
.map_or_else(|_| DEFAULT_CLOCK.to_string(), ToString::to_string);
let action = self.0;
let worlds = server.worlds.load();
let world = worlds.first().ok_or(CommandError::InvalidRequirement)?;
@@ -156,7 +156,7 @@ impl CommandExecutor for ActionExecutor {
.send_message(pumpkin_macros::translate_cross!(
translation::java::COMMANDS_TIME_SET_ABSOLUTE,
translation::bedrock::COMMANDS_TIME_SET,
TextComponent::text(clock_name.to_string()),
TextComponent::text(clock_name.clone()),
TextComponent::text(time_count.to_string())
))
.await;
@@ -171,7 +171,7 @@ impl CommandExecutor for ActionExecutor {
.send_message(pumpkin_macros::translate_cross!(
translation::java::COMMANDS_TIME_SET_ABSOLUTE,
translation::bedrock::COMMANDS_TIME_ADDED,
TextComponent::text(clock_name.to_string()),
TextComponent::text(clock_name.clone()),
TextComponent::text(total_ticks.to_string())
))
.await;
@@ -184,7 +184,7 @@ impl CommandExecutor for ActionExecutor {
.send_message(pumpkin_macros::translate_cross!(
translation::java::COMMANDS_TIME_PAUSE,
translation::bedrock::COMMANDS_TIME_STOP,
TextComponent::text(clock_name.to_string())
TextComponent::text(clock_name.clone())
))
.await;
Ok(1)
@@ -196,7 +196,7 @@ impl CommandExecutor for ActionExecutor {
.send_message(pumpkin_macros::translate_cross!(
translation::java::COMMANDS_TIME_RESUME,
translation::bedrock::COMMANDS_TIME_SET,
TextComponent::text(clock_name.to_string())
TextComponent::text(clock_name.clone())
))
.await;
Ok(1)
@@ -213,7 +213,7 @@ impl CommandExecutor for ActionExecutor {
.send_message(pumpkin_macros::translate_cross!(
translation::java::COMMANDS_TIME_RATE,
translation::bedrock::COMMANDS_TIME_SET,
TextComponent::text(clock_name.to_string()),
TextComponent::text(clock_name.clone()),
TextComponent::text(rate.to_string())
))
.await;

View File

@@ -155,13 +155,14 @@ impl CommandExecutor for StyleExecutor {
);
let uuid = entity.entity_uuid;
let style_str = match self.0 {
let style_owned = match self.0 {
StyleAction::Set => {
let style = ResourceLocationArgumentConsumer::find_arg(args, ARG_STYLE)?;
Some(style)
Some(style.to_string())
}
StyleAction::Reset => None,
};
let style_str = style_owned.as_deref();
let packet = CWaypoint::update_position(
uuid,

View File

@@ -22,8 +22,10 @@ pub mod melee_attack;
pub mod move_to_target_pos;
pub mod owner_hurt_by_target;
pub mod owner_hurt_target;
pub mod pathfind_to_raid;
pub mod pick_up_block;
pub mod place_block;
pub mod ranged_attack;
pub mod revenge;
pub mod step_and_destroy_block;
pub mod swim;

View File

@@ -0,0 +1,204 @@
use std::sync::atomic::AtomicI32;
use pumpkin_util::math::vector3::Vector3;
use crate::entity::ai::goal::{Controls, Goal, GoalFuture};
use crate::entity::ai::pathfinder::NavigatorGoal;
use crate::entity::mob::Mob;
pub struct PathfindToRaidGoal {
recruitment_tick: AtomicI32,
speed_modifier: f64,
}
impl Default for PathfindToRaidGoal {
fn default() -> Self {
Self::new(1.0)
}
}
impl PathfindToRaidGoal {
#[must_use]
pub const fn new(speed_modifier: f64) -> Self {
Self {
recruitment_tick: AtomicI32::new(0),
speed_modifier,
}
}
}
impl Goal for PathfindToRaidGoal {
fn can_start<'a>(&'a mut self, mob: &'a dyn Mob) -> GoalFuture<'a, bool> {
Box::pin(async move {
let Some(raider) = mob.as_raider() else {
return false;
};
let target = mob.get_mob_entity().target.lock().await.clone();
if target.is_some() || !raider.has_active_raid() {
return false;
}
let Some(raid_id) = raider.get_raider_data().raid_id.load() else {
return false;
};
let world = mob.get_entity().world.load();
let raids = world.raids.lock().await;
let Some(raid) = raids.get(raid_id) else {
return false;
};
if raid.is_over() {
return false;
}
let pos = mob.get_entity().block_pos.load();
let is_village = world
.villager_poi
.lock()
.await
.get_nearest_job_site(pos, 32)
.is_some();
!is_village
})
}
fn should_continue<'a>(&'a self, mob: &'a dyn Mob) -> GoalFuture<'a, bool> {
Box::pin(async move {
let Some(raider) = mob.as_raider() else {
return false;
};
let target = mob.get_mob_entity().target.lock().await.clone();
if target.is_some() || !raider.has_active_raid() {
return false;
}
let Some(raid_id) = raider.get_raider_data().raid_id.load() else {
return false;
};
let world = mob.get_entity().world.load();
let raids = world.raids.lock().await;
let Some(raid) = raids.get(raid_id) else {
return false;
};
if raid.is_over() {
return false;
}
let pos = mob.get_entity().block_pos.load();
let is_village = world
.villager_poi
.lock()
.await
.get_nearest_job_site(pos, 32)
.is_some();
!is_village
})
}
fn controls(&self) -> Controls {
Controls::MOVE
}
fn tick<'a>(&'a mut self, mob: &'a dyn Mob) -> GoalFuture<'a, ()> {
Box::pin(async move {
let Some(raider) = mob.as_raider() else {
return;
};
let Some(raid_id) = raider.get_raider_data().raid_id.load() else {
return;
};
let entity = mob.get_entity();
let world = entity.world.load();
let current_age = entity.age.load(std::sync::atomic::Ordering::Relaxed);
let raid_center = {
let raids = world.raids.lock().await;
let Some(raid) = raids.get(raid_id) else {
return;
};
if raid.is_over() {
return;
}
raid.center
};
// Periodic recruitment of nearby raiders
let next_recruit = self
.recruitment_tick
.load(std::sync::atomic::Ordering::Relaxed);
if current_age >= next_recruit {
self.recruitment_tick
.store(current_age + 20, std::sync::atomic::Ordering::Relaxed);
let bb = entity.bounding_box.load().expand(16.0, 16.0, 16.0);
let nearby = world.get_entities_at_box(&bb);
for cand in nearby {
if cand.get_entity().entity_id != entity.entity_id
&& let Some(cand_mob) = cand.get_mob()
&& let Some(cand_raider) = cand_mob.as_raider()
&& !cand_raider.has_active_raid()
&& cand_raider.can_join_raid()
{
cand_raider.get_raider_data().raid_id.store(Some(raid_id));
}
}
}
// Pathfind towards raid center if idle
let pos = entity.pos.load();
let mut nav = mob
.get_mob_entity()
.navigator
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner);
if nav.is_idle() {
// Generate a point towards the raid center
let center_vec = Vector3::new(
f64::from(raid_center.0.x) + 0.5,
f64::from(raid_center.0.y),
f64::from(raid_center.0.z) + 0.5,
);
let dir = center_vec - pos;
let dir_len = dir.x.hypot(dir.z);
let step_dist = 15.0f64.min(dir_len);
let norm_dir_x = if dir_len > 0.001 {
dir.x / dir_len
} else {
0.0
};
let norm_dir_z = if dir_len > 0.001 {
dir.z / dir_len
} else {
0.0
};
// Add slight random offset (-45 to +45 degrees)
let angle = (rand::random::<f64>() - 0.5) * std::f64::consts::FRAC_PI_2;
let cos_a = angle.cos();
let sin_a = angle.sin();
let rx = norm_dir_x * cos_a - norm_dir_z * sin_a;
let rz = norm_dir_x * sin_a + norm_dir_z * cos_a;
let dest = Vector3::new(pos.x + rx * step_dist, pos.y, pos.z + rz * step_dist);
nav.set_progress(NavigatorGoal {
current_progress: pos,
destination: dest,
speed: self.speed_modifier,
});
}
})
}
}

View File

@@ -0,0 +1,178 @@
use std::sync::{Arc, Weak};
use crate::entity::EntityBase;
use crate::entity::ai::goal::{Controls, Goal, GoalFuture};
use crate::entity::ai::pathfinder::NavigatorGoal;
use crate::entity::mob::{Mob, RangedAttackMob};
/// Generic ranged attack goal for mobs implementing [`RangedAttackMob`].
///
/// Mirrors vanilla `RangedAttackGoal`. The mob maintains distance from its target,
/// points towards it, and fires ranged attacks periodically with calculated power.
pub struct RangedAttackGoal {
mob: Weak<dyn RangedAttackMob>,
speed_modifier: f64,
attack_interval_min: i32,
attack_interval_max: i32,
attack_radius: f32,
attack_radius_sqr: f64,
see_time: i32,
attack_time: i32,
target: Option<Arc<dyn EntityBase>>,
}
impl RangedAttackGoal {
#[must_use]
pub const fn new(
mob: Weak<dyn RangedAttackMob>,
speed_modifier: f64,
attack_interval: i32,
attack_radius: f32,
) -> Self {
Self::new_with_range(
mob,
speed_modifier,
attack_interval,
attack_interval,
attack_radius,
)
}
#[must_use]
pub const fn new_with_range(
mob: Weak<dyn RangedAttackMob>,
speed_modifier: f64,
attack_interval_min: i32,
attack_interval_max: i32,
attack_radius: f32,
) -> Self {
Self {
mob,
speed_modifier,
attack_interval_min,
attack_interval_max,
attack_radius,
attack_radius_sqr: (attack_radius * attack_radius) as f64,
see_time: 0,
attack_time: -1,
target: None,
}
}
}
impl Goal for RangedAttackGoal {
fn can_start<'a>(&'a mut self, mob: &'a dyn Mob) -> GoalFuture<'a, bool> {
Box::pin(async move {
let target = mob.get_mob_entity().target.lock().await.clone();
if let Some(target) = target
&& target.get_entity().is_alive()
{
self.target = Some(target);
true
} else {
false
}
})
}
fn should_continue<'a>(&'a self, mob: &'a dyn Mob) -> GoalFuture<'a, bool> {
Box::pin(async move {
if let Some(target) = &self.target {
if !target.get_entity().is_alive() {
return false;
}
let current_target = mob.get_mob_entity().target.lock().await.clone();
current_target.is_some()
} else {
false
}
})
}
fn stop<'a>(&'a mut self, mob: &'a dyn Mob) -> GoalFuture<'a, ()> {
Box::pin(async move {
self.target = None;
self.see_time = 0;
self.attack_time = -1;
mob.get_mob_entity()
.navigator
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner)
.stop();
})
}
fn should_run_every_tick(&self) -> bool {
true
}
fn controls(&self) -> Controls {
Controls::MOVE | Controls::LOOK
}
fn tick<'a>(&'a mut self, mob: &'a dyn Mob) -> GoalFuture<'a, ()> {
Box::pin(async move {
let Some(target) = self.target.clone() else {
return;
};
let Some(ranged_mob) = self.mob.upgrade() else {
return;
};
let mob_pos = mob.get_entity().pos.load();
let target_pos = target.get_entity().pos.load();
let target_dist_sq = mob_pos.squared_distance_to_vec(&target_pos);
let has_line_of_sight = true;
if has_line_of_sight {
self.see_time += 1;
} else {
self.see_time = 0;
}
{
let mut navigator = mob
.get_mob_entity()
.navigator
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner);
if target_dist_sq <= self.attack_radius_sqr && self.see_time >= 5 {
navigator.stop();
} else {
navigator.set_progress(NavigatorGoal {
current_progress: mob_pos,
destination: target_pos,
speed: self.speed_modifier,
});
}
}
mob.get_mob_entity()
.look_control
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner)
.look_at_entity_with_range(&target, 30.0, 30.0);
self.attack_time -= 1;
if self.attack_time == 0 {
if !has_line_of_sight {
return;
}
let dist = (target_dist_sq.sqrt() as f32) / self.attack_radius;
let power = dist.clamp(0.1, 1.0);
ranged_mob.perform_ranged_attack(&target, power).await;
let min = self.attack_interval_min as f32;
let max = self.attack_interval_max as f32;
self.attack_time = (dist.mul_add(max - min, min)).floor() as i32;
} else if self.attack_time < 0 {
let ratio = (target_dist_sq.sqrt() as f32) / self.attack_radius;
let min = self.attack_interval_min as f32;
let max = self.attack_interval_max as f32;
self.attack_time = (ratio.mul_add(max - min, min)).floor() as i32;
}
})
}
}

View File

@@ -1,20 +1,75 @@
use pumpkin_data::entity::EntityType;
use std::sync::{Arc, Weak};
use crate::entity::{
Entity,
ai::goal::{look_around::RandomLookAroundGoal, look_at_entity::LookAtEntityGoal},
mob::{Mob, MobEntity},
use std::sync::{
Arc, Weak,
atomic::{AtomicBool, AtomicI32, Ordering},
};
use tokio::sync::Mutex;
use uuid::Uuid;
use pumpkin_data::{
Block,
damage::DamageType,
entity::EntityType,
item::Item,
item_stack::ItemStack,
tag::{self, Taggable},
tracked_data,
world::WorldEvent,
};
use pumpkin_nbt::compound::NbtCompound;
use pumpkin_protocol::{codec::var_int::VarInt, java::client::play::Metadata};
use pumpkin_util::{
Difficulty,
math::{position::BlockPos, vector3::Vector3},
text::TextComponent,
};
use pumpkin_world::world::BlockFlags;
use crate::{
entity::{
Entity, EntityBase, EntityBaseFuture, NbtFuture,
ai::goal::{
look_around::RandomLookAroundGoal, look_at_entity::LookAtEntityGoal,
revenge::RevengeGoal,
},
mob::{Mob, MobEntity},
projectile::wither_skull::WitherSkullEntity,
},
world::{
ExplosionInteraction,
bossbar::{Bossbar, BossbarColor, BossbarDivisions, BossbarFlags},
},
};
const INVULNERABLE_TICKS: i32 = 220;
pub struct WitherEntity {
pub mob_entity: MobEntity,
pub invulnerable_ticks: AtomicI32,
pub destroy_blocks_tick: AtomicI32,
pub next_head_update: [AtomicI32; 2],
pub idle_head_updates: [AtomicI32; 2],
pub alternative_targets: [AtomicI32; 3],
pub main_attack_timer: AtomicI32,
pub bossbar_uuid: Uuid,
pub bossbar_players: Mutex<Vec<Uuid>>,
pub dropped_loot: AtomicBool,
}
impl WitherEntity {
pub fn new(entity: Entity) -> Arc<Self> {
let mob_entity = MobEntity::new(entity);
let wither = Self { mob_entity };
let wither = Self {
mob_entity,
invulnerable_ticks: AtomicI32::new(0),
destroy_blocks_tick: AtomicI32::new(0),
next_head_update: [AtomicI32::new(0), AtomicI32::new(0)],
idle_head_updates: [AtomicI32::new(0), AtomicI32::new(0)],
alternative_targets: [AtomicI32::new(0), AtomicI32::new(0), AtomicI32::new(0)],
main_attack_timer: AtomicI32::new(0),
bossbar_uuid: Uuid::new_v4(),
bossbar_players: Mutex::new(Vec::new()),
dropped_loot: AtomicBool::new(false),
};
let mob_arc = Arc::new(wither);
let mob_weak: Weak<dyn Mob> = {
let mob_arc: Arc<dyn Mob> = mob_arc.clone();
@@ -27,21 +82,623 @@ impl WitherEntity {
.goals_selector
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner);
let mut target_selector = mob_arc
.mob_entity
.target_selector
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner);
// TODO
goal_selector.add_goal(
8,
6,
LookAtEntityGoal::with_default(mob_weak, &EntityType::PLAYER, 8.0),
);
goal_selector.add_goal(8, Box::new(RandomLookAroundGoal::default()));
goal_selector.add_goal(7, Box::new(RandomLookAroundGoal::default()));
target_selector.add_goal(1, Box::new(RevengeGoal::new(true)));
};
mob_arc
}
#[must_use]
pub fn get_invulnerable_ticks(&self) -> i32 {
self.invulnerable_ticks.load(Ordering::Relaxed)
}
pub fn set_invulnerable_ticks(&self, ticks: i32) {
self.invulnerable_ticks.store(ticks, Ordering::Relaxed);
self.mob_entity.living_entity.entity.send_meta_data(
&[Metadata::new(
tracked_data::wither::DATA_ID_INV,
VarInt(ticks),
)],
None,
);
}
#[must_use]
pub fn get_alternative_target(&self, head: usize) -> i32 {
if head < 3 {
self.alternative_targets[head].load(Ordering::Relaxed)
} else {
0
}
}
pub fn set_alternative_target(&self, head: usize, entity_id: i32) {
if head < 3 {
let old = self.alternative_targets[head].swap(entity_id, Ordering::Relaxed);
if old != entity_id {
let tracker_id = match head {
0 => tracked_data::wither::DATA_TARGET_A,
1 => tracked_data::wither::DATA_TARGET_B,
_ => tracked_data::wither::DATA_TARGET_C,
};
self.mob_entity
.living_entity
.entity
.send_meta_data(&[Metadata::new(tracker_id, VarInt(entity_id))], None);
}
}
}
#[must_use]
pub fn is_powered(&self) -> bool {
let living = &self.mob_entity.living_entity;
living.health.load() <= living.get_max_health() / 2.0
}
pub fn make_invulnerable(&self) {
self.set_invulnerable_ticks(INVULNERABLE_TICKS);
self.mob_entity
.living_entity
.set_health(self.mob_entity.living_entity.get_max_health() / 3.0);
}
#[must_use]
pub fn can_destroy(block: &Block) -> bool {
block != &Block::AIR && !block.has_tag(&tag::Block::MINECRAFT_WITHER_IMMUNE)
}
#[must_use]
pub fn get_head_x(&self, head: usize) -> f64 {
if head == 0 {
return self.mob_entity.living_entity.entity.pos.load().x;
}
let yaw = self.mob_entity.living_entity.entity.yaw.load();
let angle = (yaw + 180.0 * (head as f32 - 1.0)).to_radians();
self.mob_entity.living_entity.entity.pos.load().x + (angle.cos() as f64) * 1.3
}
#[must_use]
pub fn get_head_y(&self, head: usize) -> f64 {
let base_y = self.mob_entity.living_entity.entity.pos.load().y;
if head == 0 {
base_y + 3.0
} else {
base_y + 2.2
}
}
#[must_use]
pub fn get_head_z(&self, head: usize) -> f64 {
if head == 0 {
return self.mob_entity.living_entity.entity.pos.load().z;
}
let yaw = self.mob_entity.living_entity.entity.yaw.load();
let angle = (yaw + 180.0 * (head as f32 - 1.0)).to_radians();
self.mob_entity.living_entity.entity.pos.load().z + (angle.sin() as f64) * 1.3
}
pub async fn perform_ranged_attack(
&self,
head: usize,
target_x: f64,
target_y: f64,
target_z: f64,
dangerous: bool,
) {
let entity = &self.mob_entity.living_entity.entity;
let world = entity.world.load();
if !entity.silent.load(Ordering::Relaxed) {
world.sync_world_event(WorldEvent::SoundWitherBossShoot, entity.block_pos.load(), 0);
}
let hx = self.get_head_x(head);
let hy = self.get_head_y(head);
let hz = self.get_head_z(head);
let head_pos = Vector3::new(hx, hy, hz);
let dir = Vector3::new(target_x - hx, target_y - hy, target_z - hz);
let normalized_dir = if dir.length_squared() > 1e-6 {
dir.normalize()
} else {
Vector3::new(0.0, 0.0, 1.0)
};
let skull_entity = Entity::from_uuid(
Uuid::new_v4(),
(*world).clone(),
head_pos,
&EntityType::WITHER_SKULL,
);
let skull = Arc::new(WitherSkullEntity::new_shot(
skull_entity,
entity,
dangerous,
normalized_dir,
));
world.spawn_entity(skull).await;
}
fn make_bossbar(&self) -> Bossbar {
let title = self
.mob_entity
.living_entity
.entity
.custom_name
.load()
.as_ref()
.clone()
.unwrap_or_else(|| {
TextComponent::translate_cross(
"entity.minecraft.wither",
"entity.minecraft.wither",
[],
)
});
Bossbar {
uuid: self.bossbar_uuid,
title,
health: 1.0,
color: BossbarColor::Purple,
division: BossbarDivisions::NoDivision,
flags: BossbarFlags::DARKEN_SKY,
}
}
async fn update_bossbar(&self, world: &Arc<crate::world::World>, progress: f32) {
let pos = self.mob_entity.living_entity.entity.pos.load();
let tracking_radius_sq = 50.0 * 50.0;
let players = world.players.load();
let current: Vec<Uuid> = players
.iter()
.filter(|p| {
let p_pos = p.living_entity.entity.pos.load();
(p_pos - pos).length_squared() < tracking_radius_sq
})
.map(|p| p.gameprofile.id)
.collect();
let mut bossbar_players = self.bossbar_players.lock().await;
for &uid in &current {
if !bossbar_players.contains(&uid) {
if let Some(p) = players.iter().find(|p| p.gameprofile.id == uid) {
let mut bar = self.make_bossbar();
bar.health = progress;
p.send_bossbar(&bar).await;
}
bossbar_players.push(uid);
}
}
let to_remove: Vec<Uuid> = bossbar_players
.iter()
.filter(|uid| !current.contains(uid))
.copied()
.collect();
for uid in &to_remove {
if let Some(p) = players.iter().find(|p| &p.gameprofile.id == uid) {
p.remove_bossbar(self.bossbar_uuid).await;
}
bossbar_players.retain(|u| u != uid);
}
for player in players.iter() {
if bossbar_players.contains(&player.gameprofile.id) {
player
.update_bossbar_health(&self.bossbar_uuid, progress)
.await;
}
}
}
async fn remove_all_bossbar(&self, world: &Arc<crate::world::World>) {
let mut bossbar_players = self.bossbar_players.lock().await;
let players = world.players.load();
for player in players.iter() {
if bossbar_players.contains(&player.gameprofile.id) {
player.remove_bossbar(self.bossbar_uuid).await;
}
}
bossbar_players.clear();
}
}
impl Mob for WitherEntity {
fn get_mob_entity(&self) -> &MobEntity {
&self.mob_entity
}
fn mob_init_data_tracker(&self) -> EntityBaseFuture<'_, ()> {
Box::pin(async move {
let entity = &self.mob_entity.living_entity.entity;
entity.send_meta_data(
&[
Metadata::new(
tracked_data::wither::DATA_TARGET_A,
VarInt(self.get_alternative_target(0)),
),
Metadata::new(
tracked_data::wither::DATA_TARGET_B,
VarInt(self.get_alternative_target(1)),
),
Metadata::new(
tracked_data::wither::DATA_TARGET_C,
VarInt(self.get_alternative_target(2)),
),
Metadata::new(
tracked_data::wither::DATA_ID_INV,
VarInt(self.get_invulnerable_ticks()),
),
],
None,
);
})
}
#[expect(clippy::too_many_lines)]
fn mob_tick<'a>(&'a self, _caller: &'a Arc<dyn EntityBase>) -> EntityBaseFuture<'a, ()> {
Box::pin(async move {
let entity = &self.mob_entity.living_entity.entity;
let world = entity.world.load();
if world.level_info.load().difficulty == Difficulty::Peaceful {
self.remove_all_bossbar(&world).await;
entity.remove().await;
return;
}
if !entity.is_alive() || self.mob_entity.living_entity.health.load() <= 0.0 {
self.remove_all_bossbar(&world).await;
if !self.dropped_loot.swap(true, Ordering::SeqCst) {
let pos = entity.block_pos.load();
world
.drop_stack(&pos, ItemStack::new(1, &Item::NETHER_STAR))
.await;
}
return;
}
let invul = self.get_invulnerable_ticks();
let tick_count = entity.age.load(Ordering::Relaxed);
if invul > 0 {
let new_count = invul - 1;
let progress = (1.0 - (new_count as f32) / 220.0).clamp(0.0, 1.0);
self.update_bossbar(&world, progress).await;
if new_count <= 0 {
let pos = entity.pos.load();
let eye_y = pos.y + entity.get_eye_height();
world
.explode(
Vector3::new(pos.x, eye_y, pos.z),
7.0,
ExplosionInteraction::Mob,
)
.await;
if !entity.silent.load(Ordering::Relaxed) {
world.sync_world_event(
WorldEvent::SoundWitherBossSpawn,
entity.block_pos.load(),
0,
);
}
}
self.set_invulnerable_ticks(new_count);
if tick_count % 10 == 0 {
self.mob_entity.living_entity.heal(10.0);
}
} else {
let living = &self.mob_entity.living_entity;
let max_health = living.get_max_health();
let health = living.health.load();
let progress = if max_health > 0.0 {
(health / max_health).clamp(0.0, 1.0)
} else {
0.0
};
self.update_bossbar(&world, progress).await;
if tick_count % 20 == 0 {
living.heal(1.0);
}
// AI step - movement towards main target
let mut delta_movement = entity.velocity.load().multiply(1.0, 0.6, 1.0);
let target_opt = self.mob_entity.get_target().await;
if let Some(ref target) = target_opt {
if target.get_entity().is_alive() {
let target_pos = target.get_entity().pos.load();
let wither_pos = entity.pos.load();
let mut yd = delta_movement.y;
if wither_pos.y < target_pos.y
|| (!self.is_powered() && wither_pos.y < target_pos.y + 5.0)
{
yd = yd.max(0.0);
yd += 0.3 - yd * 0.6;
}
delta_movement.y = yd;
let delta = Vector3::new(
target_pos.x - wither_pos.x,
0.0,
target_pos.z - wither_pos.z,
);
if delta.horizontal_length_squared() > 9.0 {
let scale = delta.normalize();
delta_movement.x += scale.x * 0.3 - delta_movement.x * 0.6;
delta_movement.z += scale.z * 0.3 - delta_movement.z * 0.6;
}
self.set_alternative_target(0, target.get_entity().entity_id);
// Main head attack
let dist_sq = (wither_pos - target_pos).length_squared();
if dist_sq <= 400.0 {
let attack_timer = self.main_attack_timer.load(Ordering::Relaxed);
if attack_timer <= 0 {
self.main_attack_timer.store(40, Ordering::Relaxed);
let dangerous = rand::random_range(0.0..1.0) < 0.001;
let eye_h = target.get_entity().get_eye_height();
self.perform_ranged_attack(
0,
target_pos.x,
target_pos.y + eye_h * 0.5,
target_pos.z,
dangerous,
)
.await;
} else {
self.main_attack_timer
.store(attack_timer - 1, Ordering::Relaxed);
}
}
} else {
self.set_alternative_target(0, 0);
self.mob_entity.set_target(None).await;
}
} else {
self.set_alternative_target(0, 0);
}
entity.velocity.store(delta_movement);
if delta_movement.horizontal_length_squared() > 0.05 {
let yaw = (delta_movement.z.atan2(delta_movement.x).to_degrees() as f32) - 90.0;
entity.set_rotation(yaw, entity.pitch.load());
}
// Side heads attack logic
let difficulty = world.level_info.load().difficulty;
let wither_pos = entity.pos.load();
for i in 1..=2 {
let next_update = self.next_head_update[i - 1].load(Ordering::Relaxed);
if tick_count >= next_update {
let rand_delay = rand::random_range(0..10);
self.next_head_update[i - 1]
.store(tick_count + 10 + rand_delay, Ordering::Relaxed);
if (difficulty == Difficulty::Normal || difficulty == Difficulty::Hard)
&& self.idle_head_updates[i - 1].fetch_add(1, Ordering::Relaxed) > 15
{
let (xt, yt, zt) = (
wither_pos.x + rand::random_range(-10.0..10.0),
wither_pos.y + rand::random_range(-5.0..5.0),
wither_pos.z + rand::random_range(-10.0..10.0),
);
self.perform_ranged_attack(i, xt, yt, zt, true).await;
self.idle_head_updates[i - 1].store(0, Ordering::Relaxed);
}
let head_target_id = self.get_alternative_target(i);
if head_target_id > 0 {
let head_target = world.get_entity_by_id(head_target_id);
if let Some(target) = head_target {
let t_pos = target.get_entity().pos.load();
if target.get_entity().is_alive()
&& (wither_pos - t_pos).length_squared() <= 900.0
{
let eye_h = target.get_entity().get_eye_height();
self.perform_ranged_attack(
i,
t_pos.x,
t_pos.y + eye_h * 0.5,
t_pos.z,
false,
)
.await;
let next_delay = 40 + rand::random_range(0..20);
self.next_head_update[i - 1]
.store(tick_count + next_delay, Ordering::Relaxed);
self.idle_head_updates[i - 1].store(0, Ordering::Relaxed);
} else {
self.set_alternative_target(i, 0);
}
} else {
self.set_alternative_target(i, 0);
}
} else {
let search_box = entity.bounding_box.load().expand(20.0, 8.0, 20.0);
let entities = world.get_entities_at_box(&search_box);
let candidates: Vec<_> = entities
.into_iter()
.filter(|e| {
e.get_entity().entity_id != entity.entity_id
&& e.get_living_entity().is_some()
&& e.get_entity().is_alive()
&& !e
.get_entity()
.entity_type
.has_tag(&tag::EntityType::MINECRAFT_WITHER_FRIENDS)
})
.collect();
if !candidates.is_empty() {
let idx = rand::random_range(0..candidates.len());
self.set_alternative_target(
i,
candidates[idx].get_entity().entity_id,
);
}
}
}
}
// Block destruction
let destroy_tick = self.destroy_blocks_tick.load(Ordering::Relaxed);
if destroy_tick > 0 {
let next_destroy = destroy_tick - 1;
self.destroy_blocks_tick
.store(next_destroy, Ordering::Relaxed);
if next_destroy == 0 && world.level_info.load().game_rules.mob_griefing {
let bb = entity.bounding_box.load();
let bb_width = bb.max.x - bb.min.x;
let bb_height = bb.max.y - bb.min.y;
let width = (bb_width as f32 / 2.0 + 1.0).floor() as i32;
let height = (bb_height as f32).floor() as i32;
let min_pos = entity.block_pos.load();
let mut destroyed = false;
for dx in -width..=width {
for dy in 0..=height {
for dz in -width..=width {
let bpos = BlockPos::new(
min_pos.0.x + dx,
min_pos.0.y + dy,
min_pos.0.z + dz,
);
let block = world.get_block(&bpos);
if Self::can_destroy(block) {
world
.set_block_state(
&bpos,
Block::AIR.default_state.id,
BlockFlags::NOTIFY_ALL,
)
.await;
destroyed = true;
}
}
}
}
if destroyed && !entity.silent.load(Ordering::Relaxed) {
world.sync_world_event(
WorldEvent::SoundWitherBlockBreak,
entity.block_pos.load(),
0,
);
}
}
}
}
})
}
fn pre_damage<'a>(
&'a self,
damage_type: DamageType,
source: Option<&'a dyn EntityBase>,
) -> EntityBaseFuture<'a, bool> {
Box::pin(async move {
if damage_type.has_tag(&tag::DamageType::MINECRAFT_WITHER_IMMUNE_TO) {
return false;
}
if let Some(src) = source {
let src_type = src.get_entity().entity_type;
if src_type == &EntityType::WITHER {
return false;
}
if src_type.has_tag(&tag::EntityType::MINECRAFT_WITHER_FRIENDS) {
return false;
}
if self.is_powered()
&& (src_type == &EntityType::ARROW
|| src_type == &EntityType::SPECTRAL_ARROW
|| src_type == &EntityType::WIND_CHARGE
|| src_type == &EntityType::BREEZE_WIND_CHARGE)
{
return false;
}
}
if self.get_invulnerable_ticks() > 0
&& !damage_type.has_tag(&tag::DamageType::MINECRAFT_BYPASSES_INVULNERABILITY)
{
return false;
}
true
})
}
fn on_damage<'a>(
&'a self,
_damage_type: DamageType,
_source: Option<&'a dyn EntityBase>,
) -> EntityBaseFuture<'a, ()> {
Box::pin(async move {
if self.destroy_blocks_tick.load(Ordering::Relaxed) <= 0 {
self.destroy_blocks_tick.store(20, Ordering::Relaxed);
}
for idle in &self.idle_head_updates {
idle.fetch_add(3, Ordering::Relaxed);
}
})
}
fn post_tick(&self) -> EntityBaseFuture<'_, ()> {
Box::pin(async move {
let entity = &self.mob_entity.living_entity.entity;
if !entity.is_alive() || self.mob_entity.living_entity.health.load() <= 0.0 {
let world = entity.world.load();
self.remove_all_bossbar(&world).await;
if !self.dropped_loot.swap(true, Ordering::SeqCst) {
let pos = entity.block_pos.load();
world
.drop_stack(&pos, ItemStack::new(1, &Item::NETHER_STAR))
.await;
}
}
})
}
fn mob_write_nbt<'a>(&'a self, nbt: &'a mut NbtCompound) -> NbtFuture<'a, ()> {
Box::pin(async move {
nbt.put_int("Invul", self.get_invulnerable_ticks());
})
}
fn mob_read_nbt<'a>(&'a self, nbt: &'a NbtCompound) -> NbtFuture<'a, ()> {
Box::pin(async move {
if let Some(invul) = nbt.get_int("Invul") {
self.set_invulnerable_ticks(invul);
}
})
}
}

View File

@@ -0,0 +1,29 @@
use crate::entity::effect::{EffectFuture, MobEffect};
use crate::entity::living::LivingEntity;
pub struct HungerMobEffect;
impl MobEffect for HungerMobEffect {
fn should_apply_effect_tick(&self, duration: i32, _amplifier: u8) -> bool {
if duration <= 0 {
return false;
}
(duration as u32).is_multiple_of(20)
}
fn apply_effect_tick<'a>(
&'a self,
living: &'a LivingEntity,
amplifier: u8,
) -> EffectFuture<'a, ()> {
Box::pin(async move {
let world = living.entity.world.load();
if let Some(entity) = world.get_entity_by_id(living.entity.entity_id)
&& let Some(player) = entity.get_player()
{
let exhaustion = 0.1 * (f32::from(amplifier) + 1.0);
player.hunger_manager.add_exhaustion(exhaustion);
}
})
}
}

View File

@@ -0,0 +1,73 @@
use uuid::Uuid;
use pumpkin_data::damage::DamageType;
use pumpkin_data::entity::EntityType;
use pumpkin_data::sound::{Sound, SoundCategory};
use pumpkin_util::math::vector3::Vector3;
use crate::entity::effect::{EffectFuture, MobEffect};
use crate::entity::living::LivingEntity;
use crate::entity::r#type::from_type;
pub struct InfestedMobEffect;
impl MobEffect for InfestedMobEffect {
fn on_mob_hurt<'a>(
&'a self,
living: &'a LivingEntity,
_amplifier: u8,
_damage_type: &'a DamageType,
_damage_amount: f32,
) -> EffectFuture<'a, ()> {
Box::pin(async move {
// Wither, ender dragon and silverfish are immune
if living.entity.entity_type == &EntityType::WITHER
|| living.entity.entity_type == &EntityType::ENDER_DRAGON
|| living.entity.entity_type == &EntityType::SILVERFISH
{
return;
}
let world = living.entity.world.load();
// 10% chance to spawn
if rand::random::<f32>() <= 0.1 {
let count = rand::random::<u32>() % 2 + 1;
let bbox = living.entity.bounding_box.load();
let center = Vector3::new(
f64::midpoint(bbox.min.x, bbox.max.x),
f64::midpoint(bbox.min.y, bbox.max.y),
f64::midpoint(bbox.min.z, bbox.max.z),
);
let rot = living.entity.rotation();
let vx = rot.x * 0.3;
let vy = rot.y * 0.45;
let vz = rot.z * 0.3;
for _ in 0..count {
let random_angle = (rand::random::<f32>() - 0.5) * std::f32::consts::PI;
let cos_a = random_angle.cos();
let sin_a = random_angle.sin();
let rx = vx * cos_a + vz * sin_a;
let rz = -vx * sin_a + vz * cos_a;
let silver = from_type(&EntityType::SILVERFISH, center, &world, Uuid::new_v4());
let entity = silver.get_entity();
entity.set_pos(center);
entity.yaw.store(rand::random::<f32>() * 360.0);
entity.pitch.store(0.0);
entity.velocity.store(Vector3::new(
f64::from(rx),
f64::from(vy),
f64::from(rz),
));
world.spawn_entity(silver).await;
world.play_sound(Sound::EntitySilverfishHurt, SoundCategory::Hostile, &center);
}
}
})
}
}

View File

@@ -1,9 +1,103 @@
use crate::entity::{NBTInitFuture, NBTStorage, NBTStorageInit, NbtFuture};
pub mod hunger;
pub mod infested;
pub mod oozing;
pub mod poison;
pub mod raid_omen;
pub mod regeneration;
pub mod saturation;
pub mod weaving;
pub mod wind_charged;
pub mod wither;
use std::future::Future;
use std::pin::Pin;
use pumpkin_data::damage::DamageType;
use pumpkin_data::effect::StatusEffect;
use pumpkin_nbt::compound::NbtCompound;
use pumpkin_nbt::tag::NbtTag;
use tracing::warn;
use crate::entity::living::LivingEntity;
use crate::entity::{NBTInitFuture, NBTStorage, NBTStorageInit, NbtFuture};
pub type EffectFuture<'a, T = ()> = Pin<Box<dyn Future<Output = T> + Send + 'a>>;
pub trait MobEffect: Send + Sync {
/// Returns true if `apply_effect_tick` should be called for the current tick and duration.
fn should_apply_effect_tick(&self, _duration: i32, _amplifier: u8) -> bool {
false
}
/// Applies periodic/tick-based effect logic on a living entity.
fn apply_effect_tick<'a>(
&'a self,
_living: &'a LivingEntity,
_amplifier: u8,
) -> EffectFuture<'a, ()> {
Box::pin(async move {})
}
/// Called when an entity carrying this effect is hurt.
fn on_mob_hurt<'a>(
&'a self,
_living: &'a LivingEntity,
_amplifier: u8,
_damage_type: &'a DamageType,
_damage_amount: f32,
) -> EffectFuture<'a, ()> {
Box::pin(async move {})
}
/// Called when an entity carrying this effect dies.
fn on_mob_death<'a>(
&'a self,
_living: &'a LivingEntity,
_amplifier: u8,
_damage_type: &'a DamageType,
) -> EffectFuture<'a, ()> {
Box::pin(async move {})
}
}
pub static REGENERATION: regeneration::RegenerationMobEffect = regeneration::RegenerationMobEffect;
pub static POISON: poison::PoisonMobEffect = poison::PoisonMobEffect;
pub static WITHER: wither::WitherMobEffect = wither::WitherMobEffect;
pub static HUNGER: hunger::HungerMobEffect = hunger::HungerMobEffect;
pub static SATURATION: saturation::SaturationMobEffect = saturation::SaturationMobEffect;
pub static RAID_OMEN: raid_omen::RaidOmenMobEffect = raid_omen::RaidOmenMobEffect;
pub static INFESTED: infested::InfestedMobEffect = infested::InfestedMobEffect;
pub static OOZING: oozing::OozingMobEffect = oozing::OozingMobEffect;
pub static WEAVING: weaving::WeavingMobEffect = weaving::WeavingMobEffect;
pub static WIND_CHARGED: wind_charged::WindChargedMobEffect = wind_charged::WindChargedMobEffect;
#[must_use]
pub fn get_mob_effect(effect: &'static StatusEffect) -> Option<&'static dyn MobEffect> {
if effect == &StatusEffect::REGENERATION {
Some(&REGENERATION)
} else if effect == &StatusEffect::POISON {
Some(&POISON)
} else if effect == &StatusEffect::WITHER {
Some(&WITHER)
} else if effect == &StatusEffect::HUNGER {
Some(&HUNGER)
} else if effect == &StatusEffect::SATURATION {
Some(&SATURATION)
} else if effect == &StatusEffect::RAID_OMEN {
Some(&RAID_OMEN)
} else if effect == &StatusEffect::INFESTED {
Some(&INFESTED)
} else if effect == &StatusEffect::OOZING {
Some(&OOZING)
} else if effect == &StatusEffect::WEAVING {
Some(&WEAVING)
} else if effect == &StatusEffect::WIND_CHARGED {
Some(&WIND_CHARGED)
} else {
None
}
}
impl NBTStorage for pumpkin_data::potion::Effect {
fn write_nbt<'a>(&'a self, nbt: &'a mut NbtCompound) -> NbtFuture<'a, ()> {
Box::pin(async {

View File

@@ -0,0 +1,47 @@
use uuid::Uuid;
use pumpkin_data::damage::DamageType;
use pumpkin_data::entity::EntityType;
use pumpkin_util::math::vector3::Vector3;
use crate::entity::effect::{EffectFuture, MobEffect};
use crate::entity::living::LivingEntity;
use crate::entity::mob::slime::SlimeEntity;
use crate::entity::r#type::from_type;
pub struct OozingMobEffect;
impl MobEffect for OozingMobEffect {
fn on_mob_death<'a>(
&'a self,
living: &'a LivingEntity,
_amplifier: u8,
_damage_type: &'a DamageType,
) -> EffectFuture<'a, ()> {
Box::pin(async move {
// Slimes are immune
if living.entity.entity_type == &EntityType::SLIME {
return;
}
let world = living.entity.world.load();
let pos = living.entity.pos.load();
let spawn_pos = Vector3::new(pos.x, pos.y + 0.5, pos.z);
// Spawns 2 slimes of size 2 (medium slimes)
for _ in 0..2 {
let entity_arc = from_type(&EntityType::SLIME, spawn_pos, &world, Uuid::new_v4());
let entity = entity_arc.get_entity();
entity.set_pos(spawn_pos);
entity.yaw.store(rand::random::<f32>() * 360.0);
entity.pitch.store(0.0);
if let Some(slime) = entity_arc.cast_any().downcast_ref::<SlimeEntity>() {
slime.set_size(2, true);
}
world.spawn_entity(entity_arc).await;
}
})
}
}

View File

@@ -0,0 +1,44 @@
use pumpkin_data::damage::DamageType;
use crate::entity::effect::{EffectFuture, MobEffect};
use crate::entity::living::LivingEntity;
pub struct PoisonMobEffect;
impl MobEffect for PoisonMobEffect {
fn should_apply_effect_tick(&self, duration: i32, amplifier: u8) -> bool {
if duration <= 0 {
return false;
}
let tick_rate = 25 >> amplifier.min(4);
if tick_rate > 0 {
(duration as u32).is_multiple_of(tick_rate as u32)
} else {
true
}
}
fn apply_effect_tick<'a>(
&'a self,
living: &'a LivingEntity,
_amplifier: u8,
) -> EffectFuture<'a, ()> {
Box::pin(async move {
let current_health = living.health.load();
if current_health > 1.0
&& let Some(dyn_self) = living
.entity
.world
.load()
.get_entity_by_id(living.entity.entity_id)
{
let damage_amount = (current_health - 1.0).min(1.0);
if damage_amount > 0.0 {
dyn_self
.damage(&*dyn_self, damage_amount, DamageType::MAGIC)
.await;
}
}
})
}
}

View File

@@ -0,0 +1,32 @@
use crate::entity::EntityBase;
use crate::entity::effect::{EffectFuture, MobEffect};
use crate::entity::living::LivingEntity;
pub struct RaidOmenMobEffect;
impl MobEffect for RaidOmenMobEffect {
fn should_apply_effect_tick(&self, duration: i32, _amplifier: u8) -> bool {
duration == 1
}
fn apply_effect_tick<'a>(
&'a self,
living: &'a LivingEntity,
_amplifier: u8,
) -> EffectFuture<'a, ()> {
Box::pin(async move {
let world = living.entity.world.load();
if let Some(entity) = world.get_entity_by_id(living.entity.entity_id)
&& let Some(player) = entity.get_player()
&& !player.is_spectator()
{
let raid_pos = player
.get_raid_omen_position()
.unwrap_or_else(|| living.entity.block_pos.load());
let mut raids = world.raids.lock().await;
raids.create_or_extend_raid(player, raid_pos, &world);
player.clear_raid_omen_position();
}
})
}
}

View File

@@ -0,0 +1,32 @@
use crate::entity::effect::{EffectFuture, MobEffect};
use crate::entity::living::LivingEntity;
pub struct RegenerationMobEffect;
impl MobEffect for RegenerationMobEffect {
fn should_apply_effect_tick(&self, duration: i32, amplifier: u8) -> bool {
if duration <= 0 {
return false;
}
let tick_rate = 50 >> amplifier.min(4);
if tick_rate > 0 {
(duration as u32).is_multiple_of(tick_rate as u32)
} else {
true
}
}
fn apply_effect_tick<'a>(
&'a self,
living: &'a LivingEntity,
_amplifier: u8,
) -> EffectFuture<'a, ()> {
Box::pin(async move {
let current_health = living.health.load();
let max_health = living.get_max_health();
if current_health < max_health && current_health > 0.0 {
living.heal(1.0);
}
})
}
}

View File

@@ -0,0 +1,29 @@
use crate::entity::effect::{EffectFuture, MobEffect};
use crate::entity::living::LivingEntity;
pub struct SaturationMobEffect;
impl MobEffect for SaturationMobEffect {
fn should_apply_effect_tick(&self, _duration: i32, _amplifier: u8) -> bool {
true
}
fn apply_effect_tick<'a>(
&'a self,
living: &'a LivingEntity,
amplifier: u8,
) -> EffectFuture<'a, ()> {
Box::pin(async move {
let world = living.entity.world.load();
if let Some(entity) = world.get_entity_by_id(living.entity.entity_id)
&& let Some(player) = entity.get_player()
{
let hunger = amplifier + 1;
player.hunger_manager.add_hunger(hunger);
player
.hunger_manager
.add_saturation(f32::from(hunger) * 2.0);
}
})
}
}

View File

@@ -0,0 +1,69 @@
use std::collections::HashSet;
use pumpkin_data::Block;
use pumpkin_data::damage::DamageType;
use pumpkin_data::world::WorldEvent;
use pumpkin_util::math::position::BlockPos;
use pumpkin_util::math::vector3::Vector3;
use pumpkin_world::world::BlockFlags;
use crate::entity::effect::{EffectFuture, MobEffect};
use crate::entity::living::LivingEntity;
pub struct WeavingMobEffect;
impl MobEffect for WeavingMobEffect {
fn on_mob_death<'a>(
&'a self,
living: &'a LivingEntity,
_amplifier: u8,
_damage_type: &'a DamageType,
) -> EffectFuture<'a, ()> {
Box::pin(async move {
let world = living.entity.world.load();
// Check if mob griefing is enabled or if player
let mob_griefing = world.level_info.load().game_rules.mob_griefing;
if !living.is_player() && !mob_griefing {
return;
}
let center_pos = living.entity.block_pos.load();
let cobweb_count = (rand::random::<u32>() % 2 + 2) as usize; // 2 to 3 cobwebs
let mut positions_to_transform = HashSet::new();
// Sample up to 15 random positions in a cube of radius 1
for _ in 0..15 {
let dx = (rand::random::<u32>() % 3) as i32 - 1;
let dy = (rand::random::<u32>() % 3) as i32 - 1;
let dz = (rand::random::<u32>() % 3) as i32 - 1;
let target_pos = BlockPos(center_pos.0 + Vector3::new(dx, dy, dz));
let below_pos = BlockPos(target_pos.0 + Vector3::new(0, -1, 0));
let target_state = world.get_block_state(&target_pos);
let below_state = world.get_block_state(&below_pos);
if target_state.is_air()
&& !below_state.is_air()
&& positions_to_transform.insert(target_pos)
&& positions_to_transform.len() >= cobweb_count
{
break;
}
}
for target_pos in positions_to_transform {
world
.set_block_state(
&target_pos,
Block::COBWEB.default_state.id,
BlockFlags::NOTIFY_ALL,
)
.await;
world.sync_world_event(WorldEvent::AnimationSpawnCobweb, target_pos, 0);
}
})
}
}

View File

@@ -0,0 +1,44 @@
use pumpkin_data::damage::DamageType;
use pumpkin_data::sound::{Sound, SoundCategory};
use pumpkin_util::math::vector3::Vector3;
use crate::entity::effect::{EffectFuture, MobEffect};
use crate::entity::living::LivingEntity;
use crate::entity::projectile::wind_charge::BREEZE_WIND_CHARGE_EXPLOSION_DAMAGE_CALCULATOR;
use crate::world::explosion::ExplosionInteraction;
pub struct WindChargedMobEffect;
impl MobEffect for WindChargedMobEffect {
fn on_mob_death<'a>(
&'a self,
living: &'a LivingEntity,
_amplifier: u8,
_damage_type: &'a DamageType,
) -> EffectFuture<'a, ()> {
Box::pin(async move {
let world = living.entity.world.load();
let pos = living.entity.pos.load();
let height = living.entity.height();
let center = Vector3::new(pos.x, pos.y + f64::from(height) / 2.0, pos.z);
// gustStrength = 3.0 + random * 2.0
let gust_strength = 3.0 + rand::random::<f32>() * 2.0;
world
.explode_with_calculator(
center,
gust_strength,
ExplosionInteraction::Trigger,
Some(BREEZE_WIND_CHARGE_EXPLOSION_DAMAGE_CALCULATOR.clone()),
)
.await;
world.play_sound(
Sound::EntityBreezeWindBurst,
SoundCategory::Neutral,
&center,
);
})
}
}

View File

@@ -0,0 +1,37 @@
use pumpkin_data::damage::DamageType;
use crate::entity::effect::{EffectFuture, MobEffect};
use crate::entity::living::LivingEntity;
pub struct WitherMobEffect;
impl MobEffect for WitherMobEffect {
fn should_apply_effect_tick(&self, duration: i32, amplifier: u8) -> bool {
if duration <= 0 {
return false;
}
let tick_rate = 40 >> amplifier.min(4);
if tick_rate > 0 {
(duration as u32).is_multiple_of(tick_rate as u32)
} else {
true
}
}
fn apply_effect_tick<'a>(
&'a self,
living: &'a LivingEntity,
_amplifier: u8,
) -> EffectFuture<'a, ()> {
Box::pin(async move {
let dyn_self = living
.entity
.world
.load()
.get_entity_by_id(living.entity.entity_id);
if let Some(dyn_self) = dyn_self {
dyn_self.damage(&*dyn_self, 1.0, DamageType::WITHER).await;
}
})
}
}

View File

@@ -66,7 +66,6 @@ use pumpkin_util::text::TextComponent;
use rand::RngExt;
use std::sync::RwLock;
use tokio::sync::Mutex;
use uuid::Uuid;
/// Represents a living entity within the game world.
///
@@ -1611,6 +1610,20 @@ impl LivingEntity {
self.broadcast_death_message(&*dyn_self, damage_type, source, cause)
.await;
// Trigger on_mob_death for active status effects
let active_effects_vec: Vec<_> = {
let effects = self.active_effects.lock().await;
effects
.values()
.map(|e| (e.effect_type, e.amplifier))
.collect()
};
for (effect_type, amplifier) in active_effects_vec {
if let Some(mob_effect) = crate::entity::effect::get_mob_effect(effect_type) {
mob_effect.on_mob_death(self, amplifier, &damage_type).await;
}
}
self.reset_effects_and_attributes().await;
}
}
@@ -1790,8 +1803,10 @@ impl LivingEntity {
effect.duration
};
if Self::should_apply_effect_tick(effect, tick_duration) {
effects_to_apply.push((effect.effect_type, effect.amplifier));
if let Some(mob_effect) = crate::entity::effect::get_mob_effect(effect.effect_type)
&& mob_effect.should_apply_effect_tick(tick_duration, effect.amplifier)
{
effects_to_apply.push((mob_effect, effect.amplifier));
}
if effect.duration != -1 {
@@ -1801,110 +1816,12 @@ impl LivingEntity {
}
// Call the central removal function for each expired effect
// This will now trigger your logs and absorption resets!
for effect_type in effects_to_remove {
self.remove_effect(effect_type).await;
}
for (effect_type, amplifier) in effects_to_apply {
self.apply_effect_tick(effect_type, amplifier).await;
}
}
/// Determines if an effect should apply its tick effect this frame
/// Based on vanilla Minecraft's effect tick frequencies
///
/// TODO: villager, beacon, and other effects.
fn should_apply_effect_tick(effect: &pumpkin_data::potion::Effect, duration: i32) -> bool {
let effect_type = effect.effect_type;
if effect_type == &StatusEffect::REGENERATION {
if duration <= 0 {
return false;
}
let tick_rate = 50 >> effect.amplifier.min(4);
duration % tick_rate == 0
} else if effect_type == &StatusEffect::POISON {
if duration <= 0 {
return false;
}
let tick_rate = 25 >> effect.amplifier.min(4);
duration % tick_rate == 0
} else if effect_type == &StatusEffect::WITHER {
if duration <= 0 {
return false;
}
let tick_rate = 40 >> effect.amplifier.min(4);
duration % tick_rate == 0
} else if effect_type == &StatusEffect::HUNGER {
// Hunger every 20 ticks
duration % 20 == 0
} else if effect_type == &StatusEffect::SATURATION {
// Saturation every tick
true
} else {
// Other effects that don't tick
false
}
}
/// Applies the actual effect to the entity
/// This is called by `tick_effects` when an effect should trigger this tick
async fn apply_effect_tick(&self, effect_type: &'static StatusEffect, amplifier: u8) {
if effect_type == &StatusEffect::REGENERATION {
let current_health = self.health.load();
let max_health = self.get_max_health();
if current_health < max_health && current_health > 0.0 {
self.heal(1.0);
}
} else if effect_type == &StatusEffect::POISON {
let current_health = self.health.load();
if current_health > 1.0
&& let Some(dyn_self) = self
.entity
.world
.load()
.get_entity_by_id(self.entity.entity_id)
{
let damage_amount = (current_health - 1.0).min(1.0);
if damage_amount > 0.0 {
dyn_self
.damage(&*dyn_self, damage_amount, DamageType::MAGIC)
.await;
}
}
} else if effect_type == &StatusEffect::WITHER {
let damage_amount = 1.0;
let dyn_self = self
.entity
.world
.load()
.get_entity_by_id(self.entity.entity_id);
if let Some(dyn_self) = dyn_self {
dyn_self
.damage(&*dyn_self, damage_amount, DamageType::WITHER)
.await;
}
} else if effect_type == &StatusEffect::HUNGER {
let world = self.entity.world.load();
if let Some(entity) = world.get_entity_by_id(self.entity.entity_id)
&& let Some(player) = entity.get_player()
{
// Add exhaustion to trigger hunger decrease
let exhaustion = 0.1 * (amplifier as f32 + 1.0);
player.hunger_manager.add_exhaustion(exhaustion);
}
drop(world);
} else if effect_type == &StatusEffect::SATURATION {
let world = self.entity.world.load();
if let Some(entity) = world.get_entity_by_id(self.entity.entity_id)
&& let Some(player) = entity.get_player()
{
// Add hunger and saturation
let hunger = amplifier + 1;
player.hunger_manager.add_hunger(hunger);
player.hunger_manager.add_saturation(hunger as f32 * 2.0);
}
for (mob_effect, amplifier) in effects_to_apply {
mob_effect.apply_effect_tick(self, amplifier).await;
}
}
@@ -2146,63 +2063,6 @@ impl LivingEntity {
self.dead.store(false, Relaxed);
}
/// Try to spawn silverfish when this entity is infested and hurt.
async fn try_spawn_infested_silverfish(&self) {
if !self.has_effect(&StatusEffect::INFESTED).await {
return;
}
// Wither, ender dragon and silverfish are immune
if self.entity.entity_type == &EntityType::WITHER
|| self.entity.entity_type == &EntityType::ENDER_DRAGON
|| self.entity.entity_type == &EntityType::SILVERFISH
{
return;
}
let world = self.entity.world.load();
// 10% chance
if rand::rng().random::<f32>() <= 0.1 {
let count = rand::rng().random_range(1..3);
for _ in 0..count {
// Spawn at center of entity
let bbox = self.entity.bounding_box.load();
let center = Vector3::new(
f64::midpoint(bbox.min.x, bbox.max.x),
f64::midpoint(bbox.min.y, bbox.max.y),
f64::midpoint(bbox.min.z, bbox.max.z),
);
// Random direction
let yaw_rad = self.entity.yaw.load().to_radians() as f64;
let random_angle = rand::rng().random::<f64>() * std::f64::consts::PI
- std::f64::consts::FRAC_PI_2;
let angle = yaw_rad + random_angle;
let speed = 0.3f64;
let dx = -angle.sin() * speed;
let dz = angle.cos() * speed;
let dy = 0.1f64;
// Spawn
let silver = crate::entity::r#type::from_type(
&EntityType::SILVERFISH,
center,
&world,
Uuid::new_v4(),
);
silver.get_entity().set_pos(center);
silver.get_entity().velocity.store(Vector3::new(dx, dy, dz));
world.spawn_entity(silver).await;
// Play sound
world.play_sound(Sound::EntitySilverfishHurt, SoundCategory::Players, &center);
}
}
}
pub fn is_player(&self) -> bool {
let world = self.entity.world.load();
world.get_player_by_id(self.entity.entity_id).is_some()
@@ -2660,8 +2520,21 @@ impl EntityBase for LivingEntity {
position,
));
// Try to spawn infested silverfish
self.try_spawn_infested_silverfish().await;
// Trigger on_mob_hurt for active status effects
let active_effects_vec: Vec<_> = {
let effects = self.active_effects.lock().await;
effects
.values()
.map(|e| (e.effect_type, e.amplifier))
.collect()
};
for (effect_type, amplifier) in active_effects_vec {
if let Some(mob_effect) = crate::entity::effect::get_mob_effect(effect_type) {
mob_effect
.on_mob_hurt(self, amplifier, &damage_type, amount)
.await;
}
}
if play_sound {
world.play_sound(

View File

@@ -16,7 +16,14 @@ use crate::entity::{
look_around::RandomLookAroundGoal, look_at_entity::LookAtEntityGoal, swim::SwimGoal,
wander_around::WanderAroundGoal,
},
mob::{Mob, MobEntity},
mob::{
Mob, MobEntity,
patrol::{PatrolData, PatrollingMonster},
raider::{
ObtainRaidLeaderBannerGoal, PathfindToRaidGoal, Raider, RaiderCelebrationGoal,
RaiderData, RaiderMoveThroughVillageGoal,
},
},
projectile::evoker_fangs::EvokerFangsEntity,
r#type::from_type,
};
@@ -48,6 +55,7 @@ impl IllagerSpell {
pub struct EvokerEntity {
pub mob_entity: MobEntity,
pub raider_data: RaiderData,
spell_casting_tick_count: AtomicI32,
current_spell: AtomicU8,
wololo_target_id: Arc<Mutex<Option<i32>>>,
@@ -58,6 +66,7 @@ impl EvokerEntity {
let mob_entity = MobEntity::new(entity);
let evoker = Self {
mob_entity,
raider_data: RaiderData::default(),
spell_casting_tick_count: AtomicI32::new(0),
current_spell: AtomicU8::new(IllagerSpell::None as u8),
wololo_target_id: Arc::new(Mutex::new(None)),
@@ -78,9 +87,13 @@ impl EvokerEntity {
goal_selector.add_goal(0, Box::new(SwimGoal::default()));
goal_selector.add_goal(1, Box::new(EvokerCastingSpellGoal::new(mob_weak.clone())));
goal_selector.add_goal(2, Box::new(ObtainRaidLeaderBannerGoal));
goal_selector.add_goal(3, Box::new(RaiderMoveThroughVillageGoal::new(1.05)));
goal_selector.add_goal(3, Box::new(PathfindToRaidGoal::default()));
goal_selector.add_goal(4, Box::new(EvokerSummonSpellGoal::new(mob_weak.clone())));
goal_selector.add_goal(5, Box::new(EvokerAttackSpellGoal::new(mob_weak.clone())));
goal_selector.add_goal(6, Box::new(EvokerWololoSpellGoal::new(mob_weak)));
goal_selector.add_goal(7, Box::new(RaiderCelebrationGoal));
goal_selector.add_goal(8, Box::new(WanderAroundGoal::new(0.6)));
goal_selector.add_goal(
9,
@@ -141,14 +154,24 @@ impl EvokerEntity {
}
impl Mob for EvokerEntity {
fn as_patrolling_monster(&self) -> Option<&dyn PatrollingMonster> {
Some(self)
}
fn as_raider(&self) -> Option<&dyn Raider> {
Some(self)
}
fn mob_write_nbt<'a>(&'a self, nbt: &'a mut NbtCompound) -> NbtFuture<'a, ()> {
Box::pin(async move {
self.write_raider_nbt(nbt);
nbt.put_int("SpellTicks", self.get_spell_casting_time());
})
}
fn mob_read_nbt<'a>(&'a self, nbt: &'a NbtCompound) -> NbtFuture<'a, ()> {
Box::pin(async move {
self.read_raider_nbt(nbt);
if let Some(ticks) = nbt.get_int("SpellTicks") {
self.set_spell_casting_time(ticks);
}
@@ -170,6 +193,22 @@ impl Mob for EvokerEntity {
}
}
impl PatrollingMonster for EvokerEntity {
fn get_patrol_data(&self) -> &PatrolData {
&self.raider_data.patrol_data
}
}
impl Raider for EvokerEntity {
fn get_raider_data(&self) -> &RaiderData {
&self.raider_data
}
fn get_celebrate_sound(&self) -> Sound {
Sound::EntityEvokerCelebrate
}
}
pub struct EvokerCastingSpellGoal {
evoker: Weak<EvokerEntity>,
}

View File

@@ -1,24 +1,38 @@
use std::sync::{Arc, Weak};
use pumpkin_data::entity::EntityType;
use pumpkin_data::sound::Sound;
use pumpkin_nbt::compound::NbtCompound;
use crate::entity::{
Entity,
Entity, NbtFuture,
ai::goal::{
active_target::ActiveTargetGoal, look_around::RandomLookAroundGoal,
look_at_entity::LookAtEntityGoal, swim::SwimGoal, wander_around::WanderAroundGoal,
},
mob::{Mob, MobEntity},
mob::{
Mob, MobEntity,
patrol::{LongDistancePatrolGoal, PatrolData, PatrollingMonster},
raider::{
HoldGroundAttackGoal, ObtainRaidLeaderBannerGoal, PathfindToRaidGoal, Raider,
RaiderCelebrationGoal, RaiderData, RaiderMoveThroughVillageGoal,
},
},
};
pub struct IllusionerEntity {
pub mob_entity: MobEntity,
pub raider_data: RaiderData,
}
impl IllusionerEntity {
#[must_use]
pub fn new(entity: Entity) -> Arc<Self> {
let mob_entity = MobEntity::new(entity);
let illusioner = Self { mob_entity };
let illusioner = Self {
mob_entity,
raider_data: RaiderData::default(),
};
let mob_arc = Arc::new(illusioner);
let mob_weak: Weak<dyn Mob> = {
let mob_arc: Arc<dyn Mob> = mob_arc.clone();
@@ -33,10 +47,16 @@ impl IllusionerEntity {
.unwrap_or_else(std::sync::PoisonError::into_inner);
goal_selector.add_goal(0, Box::new(SwimGoal::default()));
goal_selector.add_goal(1, Box::new(ObtainRaidLeaderBannerGoal));
goal_selector.add_goal(2, Box::new(HoldGroundAttackGoal::new(10.0)));
goal_selector.add_goal(4, Box::new(LongDistancePatrolGoal::new(0.7, 0.595)));
goal_selector.add_goal(4, Box::new(RaiderMoveThroughVillageGoal::new(1.05)));
goal_selector.add_goal(4, Box::new(PathfindToRaidGoal::default()));
goal_selector.add_goal(5, Box::new(RaiderCelebrationGoal));
goal_selector.add_goal(5, Box::new(WanderAroundGoal::new(1.0)));
goal_selector.add_goal(
6,
LookAtEntityGoal::with_default(mob_weak.clone(), &EntityType::PLAYER, 8.0),
LookAtEntityGoal::with_default(mob_weak, &EntityType::PLAYER, 8.0),
);
goal_selector.add_goal(7, Box::new(RandomLookAroundGoal::default()));
@@ -67,4 +87,40 @@ impl Mob for IllusionerEntity {
fn get_mob_entity(&self) -> &MobEntity {
&self.mob_entity
}
fn as_patrolling_monster(&self) -> Option<&dyn PatrollingMonster> {
Some(self)
}
fn as_raider(&self) -> Option<&dyn Raider> {
Some(self)
}
fn mob_write_nbt<'a>(&'a self, nbt: &'a mut NbtCompound) -> NbtFuture<'a, ()> {
Box::pin(async move {
self.write_raider_nbt(nbt);
})
}
fn mob_read_nbt<'a>(&'a self, nbt: &'a NbtCompound) -> NbtFuture<'a, ()> {
Box::pin(async move {
self.read_raider_nbt(nbt);
})
}
}
impl PatrollingMonster for IllusionerEntity {
fn get_patrol_data(&self) -> &PatrolData {
&self.raider_data.patrol_data
}
}
impl Raider for IllusionerEntity {
fn get_raider_data(&self) -> &RaiderData {
&self.raider_data
}
fn get_celebrate_sound(&self) -> Sound {
Sound::EntityEvokerCelebrate
}
}

View File

@@ -47,10 +47,12 @@ pub mod guardian;
pub mod hoglin;
pub mod illusioner;
pub mod magma_cube;
pub mod patrol;
pub mod phantom;
pub mod piglin;
pub mod piglin_brute;
pub mod pillager;
pub mod raider;
pub mod ravager;
pub mod shulker;
pub mod silverfish;
@@ -91,8 +93,8 @@ pub struct MobEntity {
///
/// TODO: Replace with `EnvironmentAttributes::MONSTERS_BURN` lookup once the
/// `EnvironmentAttributeSystem` is implemented in `pumpkin-data`.
const NIGHT_START: i64 = 12542;
const NIGHT_END: i64 = 23459;
pub(crate) const NIGHT_START: i64 = 12542;
pub(crate) const NIGHT_END: i64 = 23459;
impl MobEntity {
const AI_DISABLED_FLAG: u8 = 1;
@@ -122,6 +124,10 @@ impl MobEntity {
}
}
pub fn has_position_target(&self) -> bool {
self.position_target_range.load(Relaxed) != -1
}
pub fn is_in_position_target_range(&self) -> bool {
self.is_in_position_target_range_pos(&self.living_entity.entity.block_pos.load())
}
@@ -636,6 +642,14 @@ pub trait Mob: EntityBase + Send + Sync {
None
}
fn as_patrolling_monster(&self) -> Option<&dyn patrol::PatrollingMonster> {
None
}
fn as_raider(&self) -> Option<&dyn raider::Raider> {
None
}
fn mob_write_nbt<'a>(&'a self, _nbt: &'a mut NbtCompound) -> NbtFuture<'a, ()> {
Box::pin(async {})
}
@@ -1246,3 +1260,11 @@ pub trait PathAwareEntity: Mob + Send + Sync {
1.0
}
}
pub trait RangedAttackMob: Mob + Send + Sync {
fn perform_ranged_attack<'a>(
&'a self,
target: &'a Arc<dyn EntityBase>,
power: f32,
) -> EntityBaseFuture<'a, ()>;
}

View File

@@ -0,0 +1,227 @@
use std::sync::atomic::{AtomicBool, Ordering};
use crossbeam::atomic::AtomicCell;
use pumpkin_data::data_component_impl::EquipmentSlot;
use pumpkin_nbt::compound::NbtCompound;
use pumpkin_util::math::position::BlockPos;
use pumpkin_util::math::vector3::Vector3;
use crate::entity::ai::goal::{Controls, Goal, GoalFuture};
use crate::entity::ai::pathfinder::NavigatorGoal;
use crate::entity::mob::Mob;
use crate::entity::mob::raider::create_ominous_banner;
pub struct PatrolData {
pub patrol_target: AtomicCell<Option<BlockPos>>,
pub patrol_leader: AtomicBool,
pub patrolling: AtomicBool,
}
impl Default for PatrolData {
fn default() -> Self {
Self {
patrol_target: AtomicCell::new(None),
patrol_leader: AtomicBool::new(false),
patrolling: AtomicBool::new(false),
}
}
}
pub trait PatrollingMonster: Mob {
fn get_patrol_data(&self) -> &PatrolData;
fn can_be_leader(&self) -> bool {
true
}
fn is_patrol_leader(&self) -> bool {
self.get_patrol_data().patrol_leader.load(Ordering::Relaxed)
}
fn set_patrol_leader(&self, is_leader: bool) {
self.get_patrol_data()
.patrol_leader
.store(is_leader, Ordering::Relaxed);
self.set_patrolling(true);
}
fn is_patrolling(&self) -> bool {
self.get_patrol_data().patrolling.load(Ordering::Relaxed)
}
fn set_patrolling(&self, patrolling: bool) {
self.get_patrol_data()
.patrolling
.store(patrolling, Ordering::Relaxed);
}
fn get_patrol_target(&self) -> Option<BlockPos> {
self.get_patrol_data().patrol_target.load()
}
fn set_patrol_target(&self, target: BlockPos) {
self.get_patrol_data().patrol_target.store(Some(target));
self.set_patrolling(true);
}
fn has_patrol_target(&self) -> bool {
self.get_patrol_data().patrol_target.load().is_some()
}
fn can_join_patrol(&self) -> bool {
true
}
fn find_patrol_target(&self) {
let block_pos = self.get_mob_entity().living_entity.entity.block_pos.load();
let dx: i32 = rand::random::<i32>().rem_euclid(1000) - 500;
let dz: i32 = rand::random::<i32>().rem_euclid(1000) - 500;
let target = BlockPos(Vector3::new(
block_pos.0.x + dx,
block_pos.0.y,
block_pos.0.z + dz,
));
self.set_patrol_target(target);
}
fn finalize_patrol_spawn(&self, is_patrol_spawn: bool) {
if !is_patrol_spawn && self.can_be_leader() {
let r: f32 = rand::random();
if r < 0.06 {
self.set_patrol_leader(true);
}
}
if self.is_patrol_leader() {
let banner = create_ominous_banner();
let living = &self.get_mob_entity().living_entity;
tokio::task::block_in_place(|| {
tokio::runtime::Handle::current().block_on(async {
let mut equipment = living.entity_equipment.lock().await;
equipment.put(&EquipmentSlot::HEAD, banner.clone());
drop(equipment);
living.send_equipment_changes(&[(EquipmentSlot::HEAD, banner)]);
});
});
}
if is_patrol_spawn {
self.set_patrolling(true);
}
}
fn write_patrol_nbt(&self, nbt: &mut NbtCompound) {
let data = self.get_patrol_data();
if let Some(target) = data.patrol_target.load() {
nbt.put_int("PatrolTargetX", target.0.x);
nbt.put_int("PatrolTargetY", target.0.y);
nbt.put_int("PatrolTargetZ", target.0.z);
}
nbt.put_bool("PatrolLeader", data.patrol_leader.load(Ordering::Relaxed));
nbt.put_bool("Patrolling", data.patrolling.load(Ordering::Relaxed));
}
fn read_patrol_nbt(&self, nbt: &NbtCompound) {
let data = self.get_patrol_data();
if let (Some(x), Some(y), Some(z)) = (
nbt.get_int("PatrolTargetX"),
nbt.get_int("PatrolTargetY"),
nbt.get_int("PatrolTargetZ"),
) {
data.patrol_target
.store(Some(BlockPos(Vector3::new(x, y, z))));
}
if let Some(leader) = nbt.get_bool("PatrolLeader") {
data.patrol_leader.store(leader, Ordering::Relaxed);
}
if let Some(patrolling) = nbt.get_bool("Patrolling") {
data.patrolling.store(patrolling, Ordering::Relaxed);
}
}
}
pub struct LongDistancePatrolGoal {
speed_modifier: f64,
leader_speed_modifier: f64,
cooldown_until: i64,
}
impl LongDistancePatrolGoal {
#[must_use]
pub const fn new(speed_modifier: f64, leader_speed_modifier: f64) -> Self {
Self {
speed_modifier,
leader_speed_modifier,
cooldown_until: -1,
}
}
}
impl Goal for LongDistancePatrolGoal {
fn can_start<'a>(&'a mut self, mob: &'a dyn Mob) -> GoalFuture<'a, bool> {
Box::pin(async move {
let Some(patrol) = mob.as_patrolling_monster() else {
return false;
};
let world = mob.get_entity().world.load();
let game_time = world.level_time.lock().await.query_daytime();
let is_on_cooldown = game_time < self.cooldown_until;
let target = mob.get_mob_entity().target.lock().await.clone();
patrol.is_patrolling()
&& target.is_none()
&& patrol.has_patrol_target()
&& !is_on_cooldown
})
}
fn should_continue<'a>(&'a self, mob: &'a dyn Mob) -> GoalFuture<'a, bool> {
Box::pin(async move {
let Some(patrol) = mob.as_patrolling_monster() else {
return false;
};
let target = mob.get_mob_entity().target.lock().await.clone();
patrol.is_patrolling() && target.is_none() && patrol.has_patrol_target()
})
}
fn controls(&self) -> Controls {
Controls::MOVE
}
fn tick<'a>(&'a mut self, mob: &'a dyn Mob) -> GoalFuture<'a, ()> {
Box::pin(async move {
let Some(patrol) = mob.as_patrolling_monster() else {
return;
};
let is_leader = patrol.is_patrol_leader();
let entity = mob.get_entity();
let pos = entity.pos.load();
let Some(patrol_target) = patrol.get_patrol_target() else {
return;
};
let dist_sq = pos.squared_distance_to_vec(&patrol_target.to_f64());
if is_leader && dist_sq < 100.0 {
patrol.find_patrol_target();
} else {
let speed = if is_leader {
self.leader_speed_modifier
} else {
self.speed_modifier
};
let mut nav = mob
.get_mob_entity()
.navigator
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner);
nav.set_progress(NavigatorGoal {
current_progress: pos,
destination: patrol_target.to_f64(),
speed,
});
}
})
}
}

View File

@@ -1,26 +1,39 @@
use std::sync::{Arc, Weak};
use pumpkin_data::entity::EntityType;
use pumpkin_data::sound::Sound;
use pumpkin_nbt::compound::NbtCompound;
use crate::entity::{
Entity,
Entity, NbtFuture,
ai::goal::{
active_target::ActiveTargetGoal, look_around::RandomLookAroundGoal,
look_at_entity::LookAtEntityGoal, melee_attack::MeleeAttackGoal, swim::SwimGoal,
wander_around::WanderAroundGoal,
},
mob::{Mob, MobEntity},
mob::{
Mob, MobEntity,
patrol::{LongDistancePatrolGoal, PatrolData, PatrollingMonster},
raider::{
HoldGroundAttackGoal, ObtainRaidLeaderBannerGoal, PathfindToRaidGoal, Raider,
RaiderCelebrationGoal, RaiderData, RaiderMoveThroughVillageGoal,
},
},
};
pub struct PillagerEntity {
pub mob_entity: MobEntity,
pub raider_data: RaiderData,
}
impl PillagerEntity {
#[must_use]
pub fn new(entity: Entity) -> Arc<Self> {
let mob_entity = MobEntity::new(entity);
let pillager = Self { mob_entity };
let mob_arc = Arc::new(pillager);
let mob_arc = Arc::new(Self {
mob_entity: MobEntity::new(entity),
raider_data: RaiderData::default(),
});
let mob_weak: Weak<dyn Mob> = {
let mob_arc: Arc<dyn Mob> = mob_arc.clone();
Arc::downgrade(&mob_arc)
@@ -34,12 +47,18 @@ impl PillagerEntity {
.unwrap_or_else(std::sync::PoisonError::into_inner);
goal_selector.add_goal(0, Box::new(SwimGoal::default()));
goal_selector.add_goal(1, Box::new(ObtainRaidLeaderBannerGoal));
goal_selector.add_goal(2, Box::new(HoldGroundAttackGoal::new(10.0)));
// Pillagers use crossbows, but for now we give them melee
goal_selector.add_goal(2, Box::new(MeleeAttackGoal::new(1.0, true)));
goal_selector.add_goal(3, Box::new(MeleeAttackGoal::new(1.0, true)));
goal_selector.add_goal(4, Box::new(LongDistancePatrolGoal::new(0.7, 0.595)));
goal_selector.add_goal(4, Box::new(RaiderMoveThroughVillageGoal::new(1.05)));
goal_selector.add_goal(4, Box::new(PathfindToRaidGoal::default()));
goal_selector.add_goal(5, Box::new(RaiderCelebrationGoal));
goal_selector.add_goal(5, Box::new(WanderAroundGoal::new(1.0)));
goal_selector.add_goal(
6,
LookAtEntityGoal::with_default(mob_weak.clone(), &EntityType::PLAYER, 8.0),
LookAtEntityGoal::with_default(mob_weak, &EntityType::PLAYER, 8.0),
);
goal_selector.add_goal(7, Box::new(RandomLookAroundGoal::default()));
@@ -70,4 +89,40 @@ impl Mob for PillagerEntity {
fn get_mob_entity(&self) -> &MobEntity {
&self.mob_entity
}
fn as_patrolling_monster(&self) -> Option<&dyn PatrollingMonster> {
Some(self)
}
fn as_raider(&self) -> Option<&dyn Raider> {
Some(self)
}
fn mob_write_nbt<'a>(&'a self, nbt: &'a mut NbtCompound) -> NbtFuture<'a, ()> {
Box::pin(async move {
self.write_raider_nbt(nbt);
})
}
fn mob_read_nbt<'a>(&'a self, nbt: &'a NbtCompound) -> NbtFuture<'a, ()> {
Box::pin(async move {
self.read_raider_nbt(nbt);
})
}
}
impl PatrollingMonster for PillagerEntity {
fn get_patrol_data(&self) -> &PatrolData {
&self.raider_data.patrol_data
}
}
impl Raider for PillagerEntity {
fn get_raider_data(&self) -> &RaiderData {
&self.raider_data
}
fn get_celebrate_sound(&self) -> Sound {
Sound::EntityPillagerCelebrate
}
}

View File

@@ -0,0 +1,418 @@
use std::sync::atomic::{AtomicBool, AtomicI32, Ordering};
use crossbeam::atomic::AtomicCell;
use pumpkin_data::data_component_impl::EquipmentSlot;
use pumpkin_data::entity::EntityType;
use pumpkin_data::item::Item;
use pumpkin_data::item_stack::ItemStack;
use pumpkin_data::sound::Sound;
use pumpkin_data::tracked_data;
use pumpkin_nbt::compound::NbtCompound;
use pumpkin_protocol::java::client::play::Metadata;
use pumpkin_util::math::vector3::Vector3;
use pumpkin_util::text::TextComponent;
use crate::entity::ai::goal::{Controls, Goal, GoalFuture};
use crate::entity::ai::pathfinder::NavigatorGoal;
use crate::entity::mob::Mob;
use crate::entity::mob::patrol::{PatrolData, PatrollingMonster};
#[must_use]
pub fn create_ominous_banner() -> ItemStack {
use pumpkin_data::data_component::DataComponent;
use pumpkin_data::data_component_impl::{CustomNameImpl, DataComponentImpl};
let mut stack = ItemStack::new(1, &Item::WHITE_BANNER);
stack.patch.push((
DataComponent::CustomName,
Some(
CustomNameImpl {
name: TextComponent::translate("block.minecraft.ominous_banner", []),
}
.to_dyn(),
),
));
stack
}
#[must_use]
pub const fn is_ominous_banner(stack: &ItemStack) -> bool {
stack.item.id == Item::WHITE_BANNER.id
}
pub struct RaiderData {
pub patrol_data: PatrolData,
pub wave: AtomicI32,
pub can_join_raid: AtomicBool,
pub ticks_outside_raid: AtomicI32,
pub is_celebrating: AtomicBool,
pub raid_id: AtomicCell<Option<i32>>,
}
impl Default for RaiderData {
fn default() -> Self {
Self {
patrol_data: PatrolData::default(),
wave: AtomicI32::new(0),
can_join_raid: AtomicBool::new(false),
ticks_outside_raid: AtomicI32::new(0),
is_celebrating: AtomicBool::new(false),
raid_id: AtomicCell::new(None),
}
}
}
pub trait Raider: PatrollingMonster {
fn get_raider_data(&self) -> &RaiderData;
fn can_join_raid(&self) -> bool {
self.get_raider_data().can_join_raid.load(Ordering::Relaxed)
}
fn set_can_join_raid(&self, can_join: bool) {
self.get_raider_data()
.can_join_raid
.store(can_join, Ordering::Relaxed);
}
fn get_wave(&self) -> i32 {
self.get_raider_data().wave.load(Ordering::Relaxed)
}
fn set_wave(&self, wave: i32) {
self.get_raider_data().wave.store(wave, Ordering::Relaxed);
}
fn is_celebrating(&self) -> bool {
self.get_raider_data()
.is_celebrating
.load(Ordering::Relaxed)
}
fn set_celebrating(&self, celebrating: bool) {
self.get_raider_data()
.is_celebrating
.store(celebrating, Ordering::Relaxed);
let entity = &self.get_mob_entity().living_entity.entity;
entity.send_meta_data(
&[Metadata::new(
tracked_data::pillager::IS_CELEBRATING,
celebrating,
)],
None,
);
}
fn has_active_raid(&self) -> bool {
self.get_raider_data().raid_id.load().is_some()
}
fn is_captain(&self) -> bool {
self.is_patrol_leader()
}
fn get_celebrate_sound(&self) -> Sound;
fn apply_raid_buffs(&self, _wave: i32, _is_captain: bool) {}
fn write_raider_nbt(&self, nbt: &mut NbtCompound) {
self.write_patrol_nbt(nbt);
let data = self.get_raider_data();
nbt.put_int("Wave", data.wave.load(Ordering::Relaxed));
nbt.put_bool("CanJoinRaid", data.can_join_raid.load(Ordering::Relaxed));
if let Some(raid_id) = data.raid_id.load() {
nbt.put_int("RaidId", raid_id);
}
}
fn read_raider_nbt(&self, nbt: &NbtCompound) {
self.read_patrol_nbt(nbt);
let data = self.get_raider_data();
if let Some(wave) = nbt.get_int("Wave") {
data.wave.store(wave, Ordering::Relaxed);
}
if let Some(can_join) = nbt.get_bool("CanJoinRaid") {
data.can_join_raid.store(can_join, Ordering::Relaxed);
}
if let Some(raid_id) = nbt.get_int("RaidId") {
data.raid_id.store(Some(raid_id));
}
}
}
/// Goal for raiders holding ground during patrols when spotting distant targets, alerting nearby raiders.
pub struct HoldGroundAttackGoal {
hostile_radius_sqr: f64,
}
impl HoldGroundAttackGoal {
#[must_use]
pub fn new(hostile_radius: f32) -> Self {
Self {
hostile_radius_sqr: f64::from(hostile_radius * hostile_radius),
}
}
}
impl Goal for HoldGroundAttackGoal {
fn can_start<'a>(&'a mut self, mob: &'a dyn Mob) -> GoalFuture<'a, bool> {
Box::pin(async move {
let Some(raider) = mob.as_raider() else {
return false;
};
if raider.has_active_raid() || !raider.is_patrolling() {
return false;
}
let target = mob.get_mob_entity().target.lock().await.clone();
target.is_some()
})
}
fn should_continue<'a>(&'a self, mob: &'a dyn Mob) -> GoalFuture<'a, bool> {
Box::pin(async move {
let target = mob.get_mob_entity().target.lock().await.clone();
target.is_some()
})
}
fn start<'a>(&'a mut self, mob: &'a dyn Mob) -> GoalFuture<'a, ()> {
Box::pin(async move {
mob.get_mob_entity()
.navigator
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner)
.stop();
let target = mob.get_mob_entity().target.lock().await.clone();
if let Some(target) = target {
let entity = mob.get_entity();
let world = entity.world.load();
let bb = entity.bounding_box.load().expand(8.0, 8.0, 8.0);
let nearby = world.get_entities_at_box(&bb);
for cand in nearby {
if cand.get_entity().entity_id != entity.entity_id
&& let Some(cand_mob) = cand.get_mob()
&& cand_mob.as_raider().is_some()
{
*cand_mob.get_mob_entity().target.lock().await = Some(target.clone());
}
}
}
})
}
fn controls(&self) -> Controls {
Controls::MOVE | Controls::LOOK
}
fn tick<'a>(&'a mut self, mob: &'a dyn Mob) -> GoalFuture<'a, ()> {
Box::pin(async move {
let target = mob.get_mob_entity().target.lock().await.clone();
if let Some(target) = target {
let mob_pos = mob.get_entity().pos.load();
let target_pos = target.get_entity().pos.load();
let dist_sq = mob_pos.squared_distance_to_vec(&target_pos);
if dist_sq > self.hostile_radius_sqr {
mob.get_mob_entity()
.look_control
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner)
.look_at_entity_with_range(&target, 30.0, 30.0);
}
}
})
}
}
/// Goal for raiders to pick up dropped ominous banners and become raid / patrol leaders.
pub struct ObtainRaidLeaderBannerGoal;
impl Goal for ObtainRaidLeaderBannerGoal {
fn can_start<'a>(&'a mut self, mob: &'a dyn Mob) -> GoalFuture<'a, bool> {
Box::pin(async move {
let Some(raider) = mob.as_raider() else {
return false;
};
if !raider.can_be_leader() || raider.is_patrol_leader() {
return false;
}
// Check if dropped banner nearby
let entity = mob.get_entity();
let world = entity.world.load();
let bb = entity.bounding_box.load().expand(16.0, 4.0, 16.0);
let nearby = world.get_entities_at_box(&bb);
nearby
.iter()
.any(|e| *e.get_entity().entity_type == EntityType::ITEM)
})
}
fn controls(&self) -> Controls {
Controls::MOVE
}
fn tick<'a>(&'a mut self, mob: &'a dyn Mob) -> GoalFuture<'a, ()> {
Box::pin(async move {
let Some(raider) = mob.as_raider() else {
return;
};
if !raider.can_be_leader() || raider.is_patrol_leader() {
return;
}
let entity = mob.get_entity();
let pos = entity.pos.load();
let world = entity.world.load();
let bb = entity.bounding_box.load().expand(16.0, 4.0, 16.0);
let nearby = world.get_entities_at_box(&bb);
for cand in nearby {
if *cand.get_entity().entity_type == EntityType::ITEM {
let cand_pos = cand.get_entity().pos.load();
let dist = pos.squared_distance_to_vec(&cand_pos);
if dist < 2.0 {
raider.set_patrol_leader(true);
let banner = create_ominous_banner();
let living = &mob.get_mob_entity().living_entity;
let mut equipment = living.entity_equipment.lock().await;
equipment.put(&EquipmentSlot::HEAD, banner.clone());
drop(equipment);
living.send_equipment_changes(&[(EquipmentSlot::HEAD, banner)]);
cand.get_entity().remove().await;
break;
}
let mut nav = mob
.get_mob_entity()
.navigator
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner);
nav.set_progress(NavigatorGoal {
current_progress: pos,
destination: cand_pos,
speed: 1.15,
});
break;
}
}
})
}
}
/// Goal for raiders celebrating victory.
pub struct RaiderCelebrationGoal;
impl Goal for RaiderCelebrationGoal {
fn can_start<'a>(&'a mut self, mob: &'a dyn Mob) -> GoalFuture<'a, bool> {
Box::pin(async move {
let Some(raider) = mob.as_raider() else {
return false;
};
let target = mob.get_mob_entity().target.lock().await.clone();
target.is_none() && raider.is_celebrating()
})
}
fn should_continue<'a>(&'a self, mob: &'a dyn Mob) -> GoalFuture<'a, bool> {
Box::pin(async move {
let Some(raider) = mob.as_raider() else {
return false;
};
let target = mob.get_mob_entity().target.lock().await.clone();
target.is_none() && raider.is_celebrating()
})
}
fn start<'a>(&'a mut self, mob: &'a dyn Mob) -> GoalFuture<'a, ()> {
Box::pin(async move {
if let Some(raider) = mob.as_raider() {
raider.set_celebrating(true);
}
})
}
fn stop<'a>(&'a mut self, mob: &'a dyn Mob) -> GoalFuture<'a, ()> {
Box::pin(async move {
if let Some(raider) = mob.as_raider() {
raider.set_celebrating(false);
}
})
}
fn tick<'a>(&'a mut self, mob: &'a dyn Mob) -> GoalFuture<'a, ()> {
Box::pin(async move {
let Some(raider) = mob.as_raider() else {
return;
};
let entity = mob.get_entity();
let pos = entity.pos.load();
let world = entity.world.load();
let r: f32 = rand::random();
if r < 0.02 && !entity.silent.load(Ordering::Relaxed) {
world.play_sound(
raider.get_celebrate_sound(),
pumpkin_data::sound::SoundCategory::Hostile,
&pos,
);
}
})
}
}
/// Goal for moving through village homes during active raids.
pub struct RaiderMoveThroughVillageGoal {
speed_modifier: f64,
}
impl RaiderMoveThroughVillageGoal {
#[must_use]
pub const fn new(speed_modifier: f64) -> Self {
Self { speed_modifier }
}
}
impl Goal for RaiderMoveThroughVillageGoal {
fn can_start<'a>(&'a mut self, mob: &'a dyn Mob) -> GoalFuture<'a, bool> {
Box::pin(async move {
let Some(raider) = mob.as_raider() else {
return false;
};
if !raider.has_active_raid() {
return false;
}
let target = mob.get_mob_entity().target.lock().await.clone();
target.is_none()
})
}
fn controls(&self) -> Controls {
Controls::MOVE
}
fn tick<'a>(&'a mut self, mob: &'a dyn Mob) -> GoalFuture<'a, ()> {
Box::pin(async move {
let entity = mob.get_entity();
let pos = entity.pos.load();
let mut nav = mob
.get_mob_entity()
.navigator
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner);
if nav.is_idle() {
let dx: f64 = (rand::random::<f64>() - 0.5) * 32.0;
let dz: f64 = (rand::random::<f64>() - 0.5) * 32.0;
let dest = Vector3::new(pos.x + dx, pos.y, pos.z + dz);
nav.set_progress(NavigatorGoal {
current_progress: pos,
destination: dest,
speed: self.speed_modifier,
});
}
})
}
}
pub use crate::entity::ai::goal::pathfind_to_raid::PathfindToRaidGoal;

View File

@@ -1,25 +1,39 @@
use std::sync::{Arc, Weak};
use pumpkin_data::entity::EntityType;
use pumpkin_data::sound::Sound;
use pumpkin_nbt::compound::NbtCompound;
use crate::entity::{
Entity,
Entity, NbtFuture,
ai::goal::{
active_target::ActiveTargetGoal, look_around::RandomLookAroundGoal,
look_at_entity::LookAtEntityGoal, melee_attack::MeleeAttackGoal, swim::SwimGoal,
wander_around::WanderAroundGoal,
},
mob::{Mob, MobEntity},
mob::{
Mob, MobEntity,
patrol::{LongDistancePatrolGoal, PatrolData, PatrollingMonster},
raider::{
HoldGroundAttackGoal, PathfindToRaidGoal, Raider, RaiderCelebrationGoal, RaiderData,
RaiderMoveThroughVillageGoal,
},
},
};
pub struct RavagerEntity {
pub mob_entity: MobEntity,
pub raider_data: RaiderData,
}
impl RavagerEntity {
#[must_use]
pub fn new(entity: Entity) -> Arc<Self> {
let mob_entity = MobEntity::new(entity);
let ravager = Self { mob_entity };
let ravager = Self {
mob_entity,
raider_data: RaiderData::default(),
};
let mob_arc = Arc::new(ravager);
let mob_weak: Weak<dyn Mob> = {
let mob_arc: Arc<dyn Mob> = mob_arc.clone();
@@ -34,11 +48,16 @@ impl RavagerEntity {
.unwrap_or_else(std::sync::PoisonError::into_inner);
goal_selector.add_goal(0, Box::new(SwimGoal::default()));
goal_selector.add_goal(2, Box::new(HoldGroundAttackGoal::new(10.0)));
goal_selector.add_goal(4, Box::new(MeleeAttackGoal::new(1.0, true)));
goal_selector.add_goal(4, Box::new(LongDistancePatrolGoal::new(0.7, 0.595)));
goal_selector.add_goal(4, Box::new(RaiderMoveThroughVillageGoal::new(1.05)));
goal_selector.add_goal(4, Box::new(PathfindToRaidGoal::default()));
goal_selector.add_goal(5, Box::new(RaiderCelebrationGoal));
goal_selector.add_goal(5, Box::new(WanderAroundGoal::new(1.0)));
goal_selector.add_goal(
6,
LookAtEntityGoal::with_default(mob_weak.clone(), &EntityType::PLAYER, 8.0),
LookAtEntityGoal::with_default(mob_weak, &EntityType::PLAYER, 8.0),
);
goal_selector.add_goal(7, Box::new(RandomLookAroundGoal::default()));
@@ -69,4 +88,44 @@ impl Mob for RavagerEntity {
fn get_mob_entity(&self) -> &MobEntity {
&self.mob_entity
}
fn as_patrolling_monster(&self) -> Option<&dyn PatrollingMonster> {
Some(self)
}
fn as_raider(&self) -> Option<&dyn Raider> {
Some(self)
}
fn mob_write_nbt<'a>(&'a self, nbt: &'a mut NbtCompound) -> NbtFuture<'a, ()> {
Box::pin(async move {
self.write_raider_nbt(nbt);
})
}
fn mob_read_nbt<'a>(&'a self, nbt: &'a NbtCompound) -> NbtFuture<'a, ()> {
Box::pin(async move {
self.read_raider_nbt(nbt);
})
}
}
impl PatrollingMonster for RavagerEntity {
fn get_patrol_data(&self) -> &PatrolData {
&self.raider_data.patrol_data
}
fn can_be_leader(&self) -> bool {
false
}
}
impl Raider for RavagerEntity {
fn get_raider_data(&self) -> &RaiderData {
&self.raider_data
}
fn get_celebrate_sound(&self) -> Sound {
Sound::EntityRavagerCelebrate
}
}

View File

@@ -1,25 +1,39 @@
use std::sync::{Arc, Weak};
use pumpkin_data::entity::EntityType;
use pumpkin_data::sound::Sound;
use pumpkin_nbt::compound::NbtCompound;
use crate::entity::{
Entity,
Entity, NbtFuture,
ai::goal::{
active_target::ActiveTargetGoal, look_around::RandomLookAroundGoal,
look_at_entity::LookAtEntityGoal, melee_attack::MeleeAttackGoal, swim::SwimGoal,
wander_around::WanderAroundGoal,
},
mob::{Mob, MobEntity},
mob::{
Mob, MobEntity,
patrol::{LongDistancePatrolGoal, PatrolData, PatrollingMonster},
raider::{
HoldGroundAttackGoal, ObtainRaidLeaderBannerGoal, PathfindToRaidGoal, Raider,
RaiderCelebrationGoal, RaiderData, RaiderMoveThroughVillageGoal,
},
},
};
pub struct VindicatorEntity {
pub mob_entity: MobEntity,
pub raider_data: RaiderData,
}
impl VindicatorEntity {
#[must_use]
pub fn new(entity: Entity) -> Arc<Self> {
let mob_entity = MobEntity::new(entity);
let vindicator = Self { mob_entity };
let vindicator = Self {
mob_entity,
raider_data: RaiderData::default(),
};
let mob_arc = Arc::new(vindicator);
let mob_weak: Weak<dyn Mob> = {
let mob_arc: Arc<dyn Mob> = mob_arc.clone();
@@ -34,11 +48,17 @@ impl VindicatorEntity {
.unwrap_or_else(std::sync::PoisonError::into_inner);
goal_selector.add_goal(0, Box::new(SwimGoal::default()));
goal_selector.add_goal(2, Box::new(MeleeAttackGoal::new(1.0, true)));
goal_selector.add_goal(1, Box::new(ObtainRaidLeaderBannerGoal));
goal_selector.add_goal(2, Box::new(HoldGroundAttackGoal::new(10.0)));
goal_selector.add_goal(3, Box::new(MeleeAttackGoal::new(1.0, true)));
goal_selector.add_goal(4, Box::new(LongDistancePatrolGoal::new(0.7, 0.595)));
goal_selector.add_goal(4, Box::new(RaiderMoveThroughVillageGoal::new(1.05)));
goal_selector.add_goal(4, Box::new(PathfindToRaidGoal::default()));
goal_selector.add_goal(5, Box::new(RaiderCelebrationGoal));
goal_selector.add_goal(5, Box::new(WanderAroundGoal::new(1.0)));
goal_selector.add_goal(
6,
LookAtEntityGoal::with_default(mob_weak.clone(), &EntityType::PLAYER, 8.0),
LookAtEntityGoal::with_default(mob_weak, &EntityType::PLAYER, 8.0),
);
goal_selector.add_goal(7, Box::new(RandomLookAroundGoal::default()));
@@ -69,4 +89,40 @@ impl Mob for VindicatorEntity {
fn get_mob_entity(&self) -> &MobEntity {
&self.mob_entity
}
fn as_patrolling_monster(&self) -> Option<&dyn PatrollingMonster> {
Some(self)
}
fn as_raider(&self) -> Option<&dyn Raider> {
Some(self)
}
fn mob_write_nbt<'a>(&'a self, nbt: &'a mut NbtCompound) -> NbtFuture<'a, ()> {
Box::pin(async move {
self.write_raider_nbt(nbt);
})
}
fn mob_read_nbt<'a>(&'a self, nbt: &'a NbtCompound) -> NbtFuture<'a, ()> {
Box::pin(async move {
self.read_raider_nbt(nbt);
})
}
}
impl PatrollingMonster for VindicatorEntity {
fn get_patrol_data(&self) -> &PatrolData {
&self.raider_data.patrol_data
}
}
impl Raider for VindicatorEntity {
fn get_raider_data(&self) -> &RaiderData {
&self.raider_data
}
fn get_celebrate_sound(&self) -> Sound {
Sound::EntityVindicatorCelebrate
}
}

View File

@@ -1,30 +1,83 @@
use std::sync::atomic::{AtomicBool, AtomicI32, Ordering};
use std::sync::{Arc, Weak};
use pumpkin_data::damage::DamageType;
use pumpkin_data::data_component_impl::EquipmentSlot;
use pumpkin_data::effect::StatusEffect;
use pumpkin_data::entity::EntityType;
use pumpkin_data::item::Item;
use pumpkin_data::item_stack::ItemStack;
use pumpkin_data::potion::Potion;
use pumpkin_data::sound::{Sound, SoundCategory};
use pumpkin_data::tracked_data;
use pumpkin_protocol::java::client::play::Metadata;
use crate::entity::{
Entity,
Entity, EntityBase, EntityBaseFuture,
ai::goal::{
active_target::ActiveTargetGoal, look_around::RandomLookAroundGoal,
look_at_entity::LookAtEntityGoal, revenge::RevengeGoal, swim::SwimGoal,
wander_around::WanderAroundGoal,
look_at_entity::LookAtEntityGoal, ranged_attack::RangedAttackGoal, revenge::RevengeGoal,
swim::SwimGoal, wander_around::WanderAroundGoal,
},
mob::{Mob, MobEntity},
mob::{
Mob, MobEntity, RangedAttackMob,
patrol::{PatrolData, PatrollingMonster},
raider::{
PathfindToRaidGoal, Raider, RaiderCelebrationGoal, RaiderData,
RaiderMoveThroughVillageGoal,
},
},
projectile::splash_potion::SplashPotionEntity,
};
fn create_potion_stack(item: &'static Item, potion: &'static Potion) -> ItemStack {
use pumpkin_data::data_component::DataComponent;
use pumpkin_data::data_component_impl::{DataComponentImpl, PotionContentsImpl};
let mut stack = ItemStack::new(1, item);
stack.patch.push((
DataComponent::PotionContents,
Some(
PotionContentsImpl {
potion_id: Some(i32::from(potion.id)),
custom_color: None,
custom_effects: Vec::new(),
custom_name: None,
}
.to_dyn(),
),
));
stack
}
/// Represents a Witch, a hostile ranged mob that throws splash potions and drinks restorative potions.
///
/// Wiki: <https://minecraft.wiki/w/Witch>
pub struct WitchEntity {
pub mob_entity: MobEntity,
pub raider_data: RaiderData,
drinking_potion: AtomicBool,
using_time: AtomicI32,
}
impl WitchEntity {
#[must_use]
pub fn new(entity: Entity) -> Arc<Self> {
let mob_entity = MobEntity::new(entity);
let witch = Self { mob_entity };
let witch = Self {
mob_entity,
raider_data: RaiderData::default(),
drinking_potion: AtomicBool::new(false),
using_time: AtomicI32::new(0),
};
let mob_arc = Arc::new(witch);
let mob_weak: Weak<dyn Mob> = {
let mob_arc: Arc<dyn Mob> = mob_arc.clone();
Arc::downgrade(&mob_arc)
};
let ranged_weak: Weak<dyn RangedAttackMob> = {
let ranged_arc: Arc<dyn RangedAttackMob> = mob_arc.clone();
Arc::downgrade(&ranged_arc)
};
{
let mut goal_selector = mob_arc
@@ -39,13 +92,19 @@ impl WitchEntity {
.unwrap_or_else(std::sync::PoisonError::into_inner);
goal_selector.add_goal(1, Box::new(SwimGoal::default()));
// TODO: WitchAttackGoal (potions)
goal_selector.add_goal(2, Box::new(WanderAroundGoal::new(1.0)));
goal_selector.add_goal(
3,
2,
Box::new(RangedAttackGoal::new(ranged_weak, 1.0, 60, 10.0)),
);
goal_selector.add_goal(3, Box::new(RaiderMoveThroughVillageGoal::new(1.05)));
goal_selector.add_goal(3, Box::new(PathfindToRaidGoal::default()));
goal_selector.add_goal(4, Box::new(RaiderCelebrationGoal));
goal_selector.add_goal(4, Box::new(WanderAroundGoal::new(1.0)));
goal_selector.add_goal(
5,
LookAtEntityGoal::with_default(mob_weak, &EntityType::PLAYER, 8.0),
);
goal_selector.add_goal(3, Box::new(RandomLookAroundGoal::default()));
goal_selector.add_goal(6, Box::new(RandomLookAroundGoal::default()));
target_selector.add_goal(1, Box::new(RevengeGoal::new(true)));
target_selector.add_goal(
@@ -56,10 +115,239 @@ impl WitchEntity {
mob_arc
}
pub fn set_drinking_potion(&self, drinking: bool) {
self.drinking_potion.store(drinking, Ordering::Relaxed);
self.mob_entity.living_entity.entity.send_meta_data(
&[Metadata::new(
tracked_data::witch::DATA_USING_ITEM,
drinking,
)],
None,
);
}
#[must_use]
pub fn is_drinking_potion(&self) -> bool {
self.drinking_potion.load(Ordering::Relaxed)
}
pub async fn throw_potion(&self, target: &Arc<dyn EntityBase>) {
if self.is_drinking_potion() {
return;
}
let entity = &self.mob_entity.living_entity.entity;
let world = entity.world.load();
let target_entity = target.get_entity();
let target_pos = target_entity.pos.load();
let target_vel = target_entity.velocity.load();
let witch_pos = entity.pos.load();
let xd = target_pos.x + target_vel.x - witch_pos.x;
let yd = target_pos.y + target_entity.get_eye_height() - 1.1 - witch_pos.y;
let zd = target_pos.z + target_vel.z - witch_pos.z;
let dist = xd.hypot(zd);
let mut potion = &Potion::HARMING;
if let Some(target_living) = target.get_living_entity() {
let r: f32 = rand::random();
if dist >= 8.0 && !target_living.has_effect(&StatusEffect::SLOWNESS).await {
potion = &Potion::SLOWNESS;
} else if target_living.health.load() >= 8.0
&& !target_living.has_effect(&StatusEffect::POISON).await
{
potion = &Potion::POISON;
} else if dist <= 3.0
&& !target_living.has_effect(&StatusEffect::WEAKNESS).await
&& r < 0.25
{
potion = &Potion::WEAKNESS;
}
}
let potion_stack = create_potion_stack(&Item::SPLASH_POTION, potion);
let splash_entity = Entity::new(world.clone(), witch_pos, &EntityType::SPLASH_POTION);
let splash = SplashPotionEntity::new_shot(splash_entity, entity);
splash.set_item_stack(potion_stack).await;
let speed = if dist <= 2.0 { 0.45 } else { 0.75 };
let yo = dist * 0.2;
splash.thrown.set_velocity(xd, yd + yo, zd, speed, 8.0);
if !entity.silent.load(Ordering::Relaxed) {
world.play_sound(Sound::EntityWitchThrow, SoundCategory::Hostile, &witch_pos);
}
let splash_arc: Arc<dyn EntityBase> = Arc::new(splash);
world.spawn_entity(splash_arc).await;
}
}
impl Mob for WitchEntity {
fn get_mob_entity(&self) -> &MobEntity {
&self.mob_entity
}
fn as_patrolling_monster(&self) -> Option<&dyn PatrollingMonster> {
Some(self)
}
fn as_raider(&self) -> Option<&dyn Raider> {
Some(self)
}
fn mob_write_nbt<'a>(
&'a self,
nbt: &'a mut pumpkin_nbt::compound::NbtCompound,
) -> crate::entity::NbtFuture<'a, ()> {
Box::pin(async move {
self.write_raider_nbt(nbt);
})
}
fn mob_read_nbt<'a>(
&'a self,
nbt: &'a pumpkin_nbt::compound::NbtCompound,
) -> crate::entity::NbtFuture<'a, ()> {
Box::pin(async move {
self.read_raider_nbt(nbt);
})
}
fn pre_damage<'a>(
&'a self,
_damage_type: DamageType,
source: Option<&'a dyn EntityBase>,
) -> EntityBaseFuture<'a, bool> {
Box::pin(async move {
if let Some(src) = source
&& src.get_entity().entity_id == self.mob_entity.living_entity.entity.entity_id
{
return false;
}
true
})
}
fn modify_incoming_damage(&self, mut amount: f32, damage_type: DamageType) -> f32 {
if damage_type == DamageType::MAGIC
|| damage_type == DamageType::INDIRECT_MAGIC
|| damage_type == DamageType::THORNS
|| damage_type == DamageType::WITHER
{
amount *= 0.15;
}
amount
}
fn mob_tick<'a>(&'a self, _caller: &'a Arc<dyn EntityBase>) -> EntityBaseFuture<'a, ()> {
Box::pin(async move {
let entity = &self.mob_entity.living_entity.entity;
let living = &self.mob_entity.living_entity;
let world = entity.world.load();
if self.is_drinking_potion() {
let remaining = self.using_time.fetch_sub(1, Ordering::Relaxed) - 1;
if remaining <= 0 {
self.set_drinking_potion(false);
let mut equipment = living.entity_equipment.lock().await;
let stack = equipment.get(&EquipmentSlot::MAIN_HAND);
equipment.put(&EquipmentSlot::MAIN_HAND, ItemStack::EMPTY.clone());
drop(equipment);
living.send_equipment_changes(&[(
EquipmentSlot::MAIN_HAND,
ItemStack::EMPTY.clone(),
)]);
let effects = crate::item::potion::PotionContents::read_potion_effects(&stack);
crate::item::potion::PotionContents::apply_effects_to(
living,
effects,
1.0,
crate::item::potion::PotionApplicationSource::Normal,
)
.await;
}
} else {
let mut potion: Option<&'static Potion> = None;
let r: f32 = rand::random();
if r < 0.15
&& entity.touching_water.load(Ordering::Relaxed)
&& !living.has_effect(&StatusEffect::WATER_BREATHING).await
{
potion = Some(&Potion::WATER_BREATHING);
} else if r < 0.15
&& entity.fire_ticks.load(Ordering::Relaxed) > 0
&& !living.has_effect(&StatusEffect::FIRE_RESISTANCE).await
{
potion = Some(&Potion::FIRE_RESISTANCE);
} else if r < 0.05 && living.health.load() < living.get_max_health() {
potion = Some(&Potion::HEALING);
} else if r < 0.5
&& let Some(target) = self.mob_entity.target.lock().await.as_ref()
&& !living.has_effect(&StatusEffect::SPEED).await
{
let target_pos = target.get_entity().pos.load();
let self_pos = entity.pos.load();
if self_pos.squared_distance_to_vec(&target_pos) > 121.0 {
potion = Some(&Potion::SWIFTNESS);
}
}
if let Some(potion) = potion {
let stack = create_potion_stack(&Item::POTION, potion);
let mut equipment = living.entity_equipment.lock().await;
equipment.put(&EquipmentSlot::MAIN_HAND, stack.clone());
drop(equipment);
living.send_equipment_changes(&[(EquipmentSlot::MAIN_HAND, stack)]);
self.using_time.store(32, Ordering::Relaxed);
self.set_drinking_potion(true);
if !entity.silent.load(Ordering::Relaxed) {
let pos = entity.pos.load();
world.play_sound(Sound::EntityWitchDrink, SoundCategory::Hostile, &pos);
}
}
}
})
}
}
impl RangedAttackMob for WitchEntity {
fn perform_ranged_attack<'a>(
&'a self,
target: &'a Arc<dyn EntityBase>,
_power: f32,
) -> EntityBaseFuture<'a, ()> {
Box::pin(async move {
self.throw_potion(target).await;
})
}
}
impl PatrollingMonster for WitchEntity {
fn get_patrol_data(&self) -> &PatrolData {
&self.raider_data.patrol_data
}
fn can_be_leader(&self) -> bool {
false
}
}
impl Raider for WitchEntity {
fn get_raider_data(&self) -> &RaiderData {
&self.raider_data
}
fn get_celebrate_sound(&self) -> Sound {
Sound::EntityWitchCelebrate
}
}

View File

@@ -1,14 +1,18 @@
use std::sync::atomic::Ordering;
use std::sync::{Arc, Weak};
use pumpkin_data::entity::EntityType;
use pumpkin_data::sound::{Sound, SoundCategory};
use crate::entity::{
Entity,
Entity, EntityBase, EntityBaseFuture,
ai::goal::{
look_around::RandomLookAroundGoal, look_at_entity::LookAtEntityGoal, swim::SwimGoal,
wander_around::WanderAroundGoal,
active_target::ActiveTargetGoal, look_around::RandomLookAroundGoal,
look_at_entity::LookAtEntityGoal, ranged_attack::RangedAttackGoal, revenge::RevengeGoal,
swim::SwimGoal, wander_around::WanderAroundGoal,
},
mob::{Mob, MobEntity},
mob::{Mob, MobEntity, RangedAttackMob},
projectile::llama_spit::LlamaSpitEntity,
};
/// Represents a Llama, a neutral mob that can be used for carrying items and spits at enemies.
@@ -27,6 +31,10 @@ impl LlamaEntity {
let mob_arc: Arc<dyn Mob> = mob_arc.clone();
Arc::downgrade(&mob_arc)
};
let ranged_weak: Weak<dyn RangedAttackMob> = {
let ranged_arc: Arc<dyn RangedAttackMob> = mob_arc.clone();
Arc::downgrade(&ranged_arc)
};
{
let mut goal_selector = mob_arc
@@ -34,18 +42,61 @@ impl LlamaEntity {
.goals_selector
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner);
let mut target_selector = mob_arc
.mob_entity
.target_selector
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner);
goal_selector.add_goal(0, Box::new(SwimGoal::default()));
goal_selector.add_goal(1, Box::new(WanderAroundGoal::new(0.7)));
goal_selector.add_goal(
2,
3,
Box::new(RangedAttackGoal::new(ranged_weak, 1.25, 40, 20.0)),
);
goal_selector.add_goal(7, Box::new(WanderAroundGoal::new(0.7)));
goal_selector.add_goal(
8,
LookAtEntityGoal::with_default(mob_weak, &EntityType::PLAYER, 6.0),
);
goal_selector.add_goal(3, Box::new(RandomLookAroundGoal::default()));
goal_selector.add_goal(9, Box::new(RandomLookAroundGoal::default()));
target_selector.add_goal(1, Box::new(RevengeGoal::new(true)));
target_selector.add_goal(
2,
ActiveTargetGoal::with_default(&mob_arc.mob_entity, &EntityType::WOLF, true),
);
};
mob_arc
}
pub async fn spit(&self, target: &Arc<dyn EntityBase>) {
let entity = self.get_entity();
let world = entity.world.load();
let spit_entity = Entity::new(world.clone(), entity.pos.load(), &EntityType::LLAMA_SPIT);
let spit = LlamaSpitEntity::new_shot(spit_entity, entity);
let mob_pos = entity.pos.load();
let target_entity = target.get_entity();
let target_pos = target_entity.pos.load();
let target_height = f64::from(target_entity.entity_dimension.load().height);
let dx = target_pos.x - mob_pos.x;
let dy = (target_pos.y + target_height / 3.0) - spit.get_entity().pos.load().y;
let dz = target_pos.z - mob_pos.z;
let horizontal_distance = dx.hypot(dz);
let yo = horizontal_distance * 0.2;
spit.thrown.set_velocity(dx, dy + yo, dz, 1.5, 10.0);
if !entity.silent.load(Ordering::Relaxed) {
world.play_sound(Sound::EntityLlamaSpit, SoundCategory::Neutral, &mob_pos);
}
let spit_arc: Arc<dyn EntityBase> = Arc::new(spit);
world.spawn_entity(spit_arc).await;
}
}
impl Mob for LlamaEntity {
@@ -53,3 +104,15 @@ impl Mob for LlamaEntity {
&self.mob_entity
}
}
impl RangedAttackMob for LlamaEntity {
fn perform_ranged_attack<'a>(
&'a self,
target: &'a Arc<dyn EntityBase>,
_power: f32,
) -> EntityBaseFuture<'a, ()> {
Box::pin(async move {
self.spit(target).await;
})
}
}

View File

@@ -1,14 +1,18 @@
use std::sync::atomic::Ordering;
use std::sync::{Arc, Weak};
use pumpkin_data::entity::EntityType;
use pumpkin_data::sound::{Sound, SoundCategory};
use crate::entity::{
Entity,
Entity, EntityBase, EntityBaseFuture,
ai::goal::{
active_target::ActiveTargetGoal, look_around::RandomLookAroundGoal,
look_at_entity::LookAtEntityGoal, wander_around::WanderAroundGoal,
look_at_entity::LookAtEntityGoal, ranged_attack::RangedAttackGoal,
wander_around::WanderAroundGoal,
},
mob::{Mob, MobEntity},
mob::{Mob, MobEntity, RangedAttackMob},
projectile::snowball::SnowballEntity,
};
pub struct SnowGolemEntity {
@@ -24,6 +28,10 @@ impl SnowGolemEntity {
let mob_arc: Arc<dyn Mob> = mob_arc.clone();
Arc::downgrade(&mob_arc)
};
let ranged_weak: Weak<dyn RangedAttackMob> = {
let ranged_arc: Arc<dyn RangedAttackMob> = mob_arc.clone();
Arc::downgrade(&ranged_arc)
};
{
let mut goal_selector = mob_arc
@@ -37,7 +45,10 @@ impl SnowGolemEntity {
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner);
// TODO: SnowballAttackGoal
goal_selector.add_goal(
1,
Box::new(RangedAttackGoal::new(ranged_weak, 1.25, 20, 10.0)),
);
goal_selector.add_goal(5, Box::new(WanderAroundGoal::new(1.0)));
goal_selector.add_goal(
6,
@@ -53,6 +64,34 @@ impl SnowGolemEntity {
mob_arc
}
pub async fn throw_snowball(&self, target: &Arc<dyn EntityBase>) {
let entity = self.get_entity();
let world = entity.world.load();
let snowball_entity = Entity::new(world.clone(), entity.pos.load(), &EntityType::SNOWBALL);
let snowball = SnowballEntity::new_shot(snowball_entity, entity);
let mob_pos = entity.pos.load();
let target_entity = target.get_entity();
let target_pos = target_entity.pos.load();
let target_height = f64::from(target_entity.entity_dimension.load().height);
let dx = target_pos.x - mob_pos.x;
let dy = (target_pos.y + target_height / 3.0) - snowball.get_entity().pos.load().y;
let dz = target_pos.z - mob_pos.z;
let horizontal_distance = dx.hypot(dz);
let yo = horizontal_distance * 0.2;
snowball.thrown.set_velocity(dx, dy + yo, dz, 1.6, 12.0);
if !entity.silent.load(Ordering::Relaxed) {
world.play_sound(Sound::EntitySnowballThrow, SoundCategory::Neutral, &mob_pos);
}
let snowball_arc: Arc<dyn EntityBase> = Arc::new(snowball);
world.spawn_entity(snowball_arc).await;
}
}
impl Mob for SnowGolemEntity {
@@ -60,3 +99,15 @@ impl Mob for SnowGolemEntity {
&self.mob_entity
}
}
impl RangedAttackMob for SnowGolemEntity {
fn perform_ranged_attack<'a>(
&'a self,
target: &'a Arc<dyn EntityBase>,
_power: f32,
) -> EntityBaseFuture<'a, ()> {
Box::pin(async move {
self.throw_snowball(target).await;
})
}
}

View File

@@ -1,14 +1,18 @@
use std::sync::atomic::Ordering;
use std::sync::{Arc, Weak};
use pumpkin_data::entity::EntityType;
use pumpkin_data::sound::{Sound, SoundCategory};
use crate::entity::{
Entity,
Entity, EntityBase, EntityBaseFuture,
ai::goal::{
look_around::RandomLookAroundGoal, look_at_entity::LookAtEntityGoal, swim::SwimGoal,
wander_around::WanderAroundGoal,
active_target::ActiveTargetGoal, look_around::RandomLookAroundGoal,
look_at_entity::LookAtEntityGoal, ranged_attack::RangedAttackGoal, revenge::RevengeGoal,
swim::SwimGoal, wander_around::WanderAroundGoal,
},
mob::{Mob, MobEntity},
mob::{Mob, MobEntity, RangedAttackMob},
projectile::llama_spit::LlamaSpitEntity,
};
pub struct TraderLlamaEntity {
@@ -24,6 +28,10 @@ impl TraderLlamaEntity {
let mob_arc: Arc<dyn Mob> = mob_arc.clone();
Arc::downgrade(&mob_arc)
};
let ranged_weak: Weak<dyn RangedAttackMob> = {
let ranged_arc: Arc<dyn RangedAttackMob> = mob_arc.clone();
Arc::downgrade(&ranged_arc)
};
{
let mut goal_selector = mob_arc
@@ -31,18 +39,61 @@ impl TraderLlamaEntity {
.goals_selector
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner);
let mut target_selector = mob_arc
.mob_entity
.target_selector
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner);
goal_selector.add_goal(0, Box::new(SwimGoal::default()));
goal_selector.add_goal(1, Box::new(WanderAroundGoal::new(0.7)));
goal_selector.add_goal(
2,
3,
Box::new(RangedAttackGoal::new(ranged_weak, 1.25, 40, 20.0)),
);
goal_selector.add_goal(7, Box::new(WanderAroundGoal::new(0.7)));
goal_selector.add_goal(
8,
LookAtEntityGoal::with_default(mob_weak, &EntityType::PLAYER, 6.0),
);
goal_selector.add_goal(3, Box::new(RandomLookAroundGoal::default()));
goal_selector.add_goal(9, Box::new(RandomLookAroundGoal::default()));
target_selector.add_goal(1, Box::new(RevengeGoal::new(true)));
target_selector.add_goal(
2,
ActiveTargetGoal::with_default(&mob_arc.mob_entity, &EntityType::WOLF, true),
);
};
mob_arc
}
pub async fn spit(&self, target: &Arc<dyn EntityBase>) {
let entity = self.get_entity();
let world = entity.world.load();
let spit_entity = Entity::new(world.clone(), entity.pos.load(), &EntityType::LLAMA_SPIT);
let spit = LlamaSpitEntity::new_shot(spit_entity, entity);
let mob_pos = entity.pos.load();
let target_entity = target.get_entity();
let target_pos = target_entity.pos.load();
let target_height = f64::from(target_entity.entity_dimension.load().height);
let dx = target_pos.x - mob_pos.x;
let dy = (target_pos.y + target_height / 3.0) - spit.get_entity().pos.load().y;
let dz = target_pos.z - mob_pos.z;
let horizontal_distance = dx.hypot(dz);
let yo = horizontal_distance * 0.2;
spit.thrown.set_velocity(dx, dy + yo, dz, 1.5, 10.0);
if !entity.silent.load(Ordering::Relaxed) {
world.play_sound(Sound::EntityLlamaSpit, SoundCategory::Neutral, &mob_pos);
}
let spit_arc: Arc<dyn EntityBase> = Arc::new(spit);
world.spawn_entity(spit_arc).await;
}
}
impl Mob for TraderLlamaEntity {
@@ -50,3 +101,15 @@ impl Mob for TraderLlamaEntity {
&self.mob_entity
}
}
impl RangedAttackMob for TraderLlamaEntity {
fn perform_ranged_attack<'a>(
&'a self,
target: &'a Arc<dyn EntityBase>,
_power: f32,
) -> EntityBaseFuture<'a, ()> {
Box::pin(async move {
self.spit(target).await;
})
}
}

View File

@@ -57,7 +57,7 @@ pub use data::{
get_food_points,
};
async fn trigger_trade_advancement(player: &Player) {
pub(crate) async fn trigger_trade_advancement(player: &Player) {
player
.trigger_advancement(
crate::entity::player::advancement::trigger::AdvancementTrigger::TradedWithVillager,
@@ -166,7 +166,7 @@ fn enchant_trade_item(
Some((stack, additional_cost))
}
fn apply_random_dye(rng: &mut impl rand::Rng, stack: &mut ItemStack) {
pub(crate) fn apply_random_dye(rng: &mut impl rand::Rng, stack: &mut ItemStack) {
use pumpkin_data::data_component::DataComponent;
use pumpkin_data::data_component_impl::{DataComponentImpl, DyedColorImpl};
use rand::RngExt;
@@ -199,7 +199,7 @@ fn apply_random_dye(rng: &mut impl rand::Rng, stack: &mut ItemStack) {
));
}
fn apply_random_stew_effect(rng: &mut impl rand::Rng, stack: &mut ItemStack) {
pub(crate) fn apply_random_stew_effect(rng: &mut impl rand::Rng, stack: &mut ItemStack) {
use pumpkin_data::data_component::DataComponent;
use pumpkin_data::data_component_impl::{
DataComponentImpl, SuspiciousStewEffect, SuspiciousStewEffectsImpl,
@@ -230,7 +230,7 @@ fn apply_random_stew_effect(rng: &mut impl rand::Rng, stack: &mut ItemStack) {
));
}
fn apply_potion(stack: &mut ItemStack, potion_name: &str) {
pub(crate) fn apply_potion(stack: &mut ItemStack, potion_name: &str) {
use pumpkin_data::data_component::DataComponent;
use pumpkin_data::data_component_impl::{DataComponentImpl, PotionContentsImpl};

File diff suppressed because it is too large Load Diff

View File

@@ -712,6 +712,8 @@ pub struct Player {
pub open_container: AtomicCell<Option<u64>>,
/// The block position of the currently open container screen (if any).
pub open_container_pos: AtomicCell<Option<BlockPos>>,
/// The village position where Raid Omen was triggered.
pub raid_omen_position: AtomicCell<Option<BlockPos>>,
/// The item currently being held by the player.
pub carried_item: Mutex<Option<ItemStack>>,
/// The player's abilities and special powers.
@@ -978,6 +980,7 @@ impl Player {
enchantment_seed: AtomicI32::new(rand::random()),
open_container: AtomicCell::new(None),
open_container_pos: AtomicCell::new(None),
raid_omen_position: AtomicCell::new(None),
tick_counter: AtomicI32::new(0),
start_mining_time: AtomicI32::new(0),
last_input: AtomicI8::new(0),
@@ -2503,6 +2506,7 @@ impl Player {
// experience handling
self.tick_experience().await;
self.tick_health().await;
self.tick_raid_omen().await;
self.tick_maps(server).await;
// Anti-spam counter decay
@@ -3771,6 +3775,51 @@ impl Player {
}
}
pub async fn tick_raid_omen(&self) {
if self.is_spectator() {
return;
}
if let Some(bad_omen) = self.get_effect(&StatusEffect::BAD_OMEN).await
&& !self.has_effect(&StatusEffect::RAID_OMEN).await
{
let world = self.world();
let player_pos = self.living_entity.entity.block_pos.load();
let pos_f64 = self.living_entity.entity.pos.load();
let village_pos = world
.villager_poi
.lock()
.await
.get_nearest_job_site(player_pos, 64)
.or_else(|| {
world.raids.try_lock().ok().and_then(|raids| {
raids
.get_nearby_raid(&player_pos, 64.0 * 64.0)
.map(|r| r.center)
})
});
if let Some(pos) = village_pos {
self.living_entity
.remove_effect(&StatusEffect::BAD_OMEN)
.await;
self.set_raid_omen_position(pos);
let effect = Effect {
effect_type: &StatusEffect::RAID_OMEN,
duration: 600,
amplifier: bad_omen.amplifier,
ambient: false,
show_particles: true,
show_icon: true,
blend: true,
};
self.add_effect(effect).await;
world.play_sound(Sound::BlockBellResonate, SoundCategory::Neutral, &pos_f64);
}
}
}
pub async fn set_health(&self, health: f32) {
self.living_entity.set_health(health);
self.send_health().await;
@@ -4532,6 +4581,19 @@ impl Player {
effects.values().cloned().collect()
}
#[must_use]
pub fn get_raid_omen_position(&self) -> Option<BlockPos> {
self.raid_omen_position.load()
}
pub fn set_raid_omen_position(&self, pos: BlockPos) {
self.raid_omen_position.store(Some(pos));
}
pub fn clear_raid_omen_position(&self) {
self.raid_omen_position.store(None);
}
pub async fn send_active_effects(&self) {
let effects = self.living_entity.active_effects.lock().await;
for effect in effects.values() {

View File

@@ -0,0 +1,114 @@
use std::sync::Arc;
use std::sync::atomic::{AtomicBool, Ordering};
use pumpkin_data::damage::DamageType;
use pumpkin_util::math::vector3::Vector3;
use crate::{
entity::{
Entity, EntityBase, EntityBaseFuture,
living::LivingEntity,
projectile::{ProjectileHit, ThrownItemEntity},
},
server::Server,
};
pub const LLAMA_SPIT_GRAVITY: f64 = 0.06;
pub struct LlamaSpitEntity {
pub thrown: ThrownItemEntity,
}
impl LlamaSpitEntity {
#[must_use]
pub const fn new(entity: Entity) -> Self {
let thrown = ThrownItemEntity {
entity,
owner_id: None,
collides_with_projectiles: false,
has_hit: AtomicBool::new(false),
gravity: LLAMA_SPIT_GRAVITY,
};
Self { thrown }
}
#[must_use]
pub fn new_shot(entity: Entity, shooter: &Entity) -> Self {
let owner_pos = shooter.pos.load();
let body_yaw_rad = f64::from(shooter.body_yaw.load()).to_radians();
let bb_width = f64::from(shooter.entity_dimension.load().width);
let offset = f64::midpoint(bb_width, 1.0);
let x = owner_pos.x - offset * body_yaw_rad.sin();
let y = owner_pos.y + shooter.get_eye_height() - 0.1;
let z = owner_pos.z + offset * body_yaw_rad.cos();
entity.pos.store(Vector3::new(x, y, z));
let thrown = ThrownItemEntity {
entity,
owner_id: Some(shooter.entity_id),
collides_with_projectiles: false,
has_hit: AtomicBool::new(false),
gravity: LLAMA_SPIT_GRAVITY,
};
Self { thrown }
}
}
impl EntityBase for LlamaSpitEntity {
fn tick<'a>(
&'a self,
caller: &'a Arc<dyn EntityBase>,
server: &'a Server,
) -> EntityBaseFuture<'a, ()> {
Box::pin(async move {
if self.get_entity().touching_water.load(Ordering::Relaxed) {
self.get_entity().remove().await;
return;
}
self.thrown.process_tick(caller, server).await;
})
}
fn get_entity(&self) -> &Entity {
self.thrown.get_entity()
}
fn get_living_entity(&self) -> Option<&LivingEntity> {
None
}
fn cast_any(&self) -> &dyn std::any::Any {
self
}
fn on_hit(&self, hit: ProjectileHit) -> EntityBaseFuture<'_, ()> {
Box::pin(async move {
if let ProjectileHit::Entity {
ref entity,
hit_pos,
..
} = hit
{
let entity_clone = entity.clone();
let world = self.get_entity().world.load();
let owner_id = self.thrown.owner_id;
let owner = owner_id.and_then(|id| world.get_entity_by_id(id));
tokio::spawn(async move {
let _ = entity_clone
.damage_with_context(
entity_clone.as_ref(),
1.0,
DamageType::SPIT,
Some(hit_pos),
None,
owner.as_deref(),
)
.await;
});
}
})
}
}

View File

@@ -18,12 +18,14 @@ pub mod fireball;
pub mod firework_rocket;
pub mod fishing_bobber;
pub mod lingering_potion;
pub mod llama_spit;
pub mod shulker_bullet;
pub mod small_fireball;
pub mod snowball;
pub mod splash_potion;
pub mod trident;
pub mod wind_charge;
pub mod wither_skull;
#[must_use]
pub fn is_projectile(entity_type: &EntityType) -> bool {
@@ -40,6 +42,8 @@ pub fn is_projectile(entity_type: &EntityType) -> bool {
|| *entity_type == EntityType::FIREBALL
|| *entity_type == EntityType::SMALL_FIREBALL
|| *entity_type == EntityType::FISHING_BOBBER
|| *entity_type == EntityType::WITHER_SKULL
|| *entity_type == EntityType::LLAMA_SPIT
}
pub struct ThrownItemEntity {

View File

@@ -0,0 +1,176 @@
use std::sync::Arc;
use std::sync::atomic::{AtomicBool, Ordering};
use pumpkin_data::{damage::DamageType, effect::StatusEffect, potion::Effect, tracked_data};
use pumpkin_nbt::compound::NbtCompound;
use pumpkin_protocol::java::client::play::Metadata;
use pumpkin_util::{Difficulty, math::vector3::Vector3};
use crate::{
entity::{
Entity, EntityBase, EntityBaseFuture, NbtFuture,
projectile::{ProjectileHit, ThrownItemEntity},
},
server::Server,
world::ExplosionInteraction,
};
const GRAVITY: f64 = 0.0;
pub struct WitherSkullEntity {
pub thrown: ThrownItemEntity,
pub dangerous: AtomicBool,
}
impl WitherSkullEntity {
#[must_use]
pub const fn new(entity: Entity) -> Self {
let thrown = ThrownItemEntity {
entity,
owner_id: None,
collides_with_projectiles: false,
has_hit: AtomicBool::new(false),
gravity: GRAVITY,
};
Self {
thrown,
dangerous: AtomicBool::new(false),
}
}
#[must_use]
pub fn new_shot(
entity: Entity,
shooter: &Entity,
dangerous: bool,
direction: Vector3<f64>,
) -> Self {
let thrown = ThrownItemEntity::new(entity, shooter, GRAVITY);
let speed = 0.95;
let vel = direction.normalize().multiply(speed, speed, speed);
thrown.entity.velocity.store(vel);
let len = vel.horizontal_length();
thrown.entity.set_rotation(
vel.x.atan2(vel.z) as f32 * 57.295_776,
vel.y.atan2(len) as f32 * 57.295_776,
);
Self {
thrown,
dangerous: AtomicBool::new(dangerous),
}
}
#[must_use]
pub fn is_dangerous(&self) -> bool {
self.dangerous.load(Ordering::Relaxed)
}
pub fn set_dangerous(&self, dangerous: bool) {
self.dangerous.store(dangerous, Ordering::Relaxed);
self.thrown.entity.send_meta_data(
&[Metadata::new(
tracked_data::wither_skull::DATA_DANGEROUS,
dangerous,
)],
None,
);
}
}
impl EntityBase for WitherSkullEntity {
fn write_custom_nbt<'a>(&'a self, nbt: &'a mut NbtCompound) -> NbtFuture<'a, ()> {
Box::pin(async move {
nbt.put_bool("dangerous", self.is_dangerous());
})
}
fn read_custom_nbt<'a>(&'a self, nbt: &'a NbtCompound) -> NbtFuture<'a, ()> {
Box::pin(async move {
if let Some(dangerous) = nbt.get_bool("dangerous") {
self.dangerous.store(dangerous, Ordering::Relaxed);
}
})
}
fn init_data_tracker(&self) -> EntityBaseFuture<'_, ()> {
Box::pin(async move {
let entity = self.get_entity();
entity.send_meta_data(
&[Metadata::new(
tracked_data::wither_skull::DATA_DANGEROUS,
self.is_dangerous(),
)],
None,
);
})
}
fn tick<'a>(
&'a self,
caller: &'a Arc<dyn EntityBase>,
server: &'a Server,
) -> EntityBaseFuture<'a, ()> {
Box::pin(async move {
self.thrown.process_tick(caller, server).await;
})
}
fn get_entity(&self) -> &Entity {
self.thrown.get_entity()
}
fn get_living_entity(&self) -> Option<&crate::entity::living::LivingEntity> {
None
}
fn cast_any(&self) -> &dyn std::any::Any {
self
}
fn on_hit(&self, hit: ProjectileHit) -> EntityBaseFuture<'_, ()> {
Box::pin(async move {
let world = self.get_entity().world.load();
if let ProjectileHit::Entity { ref entity, .. } = hit {
let entity_clone = entity.clone();
let difficulty = world.level_info.load().difficulty;
tokio::spawn(async move {
let _ = entity_clone
.damage(entity_clone.as_ref(), 8.0, DamageType::WITHER_SKULL)
.await;
if let Some(living) = entity_clone.get_living_entity() {
let duration = match difficulty {
Difficulty::Hard => 800, // 40 seconds
Difficulty::Normal => 200, // 10 seconds
Difficulty::Easy | Difficulty::Peaceful => 0,
};
if duration > 0 {
let effect = Effect {
effect_type: &StatusEffect::WITHER,
duration,
amplifier: 1,
ambient: false,
show_particles: true,
show_icon: true,
blend: true,
};
if let Some(player) = entity_clone.get_player() {
player.send_effect(effect.clone()).await;
}
living.add_effect(effect).await;
}
}
});
}
let hit_pos = hit.hit_pos();
world.explode(hit_pos, 1.0, ExplosionInteraction::Mob).await;
})
}
}

View File

@@ -111,12 +111,14 @@ use crate::entity::projectile::eye_of_ender::EyeOfEnder;
use crate::entity::projectile::fireball::FireballEntity;
use crate::entity::projectile::firework_rocket::FireworkRocketEntity;
use crate::entity::projectile::lingering_potion::LingeringPotionEntity;
use crate::entity::projectile::llama_spit::LlamaSpitEntity;
use crate::entity::projectile::shulker_bullet::ShulkerBulletEntity;
use crate::entity::projectile::small_fireball::SmallFireballEntity;
use crate::entity::projectile::snowball::SnowballEntity;
use crate::entity::projectile::splash_potion::SplashPotionEntity;
use crate::entity::projectile::trident::TridentEntity;
use crate::entity::projectile::wind_charge::{WIND_CHARGE_GRAVITY, WindChargeEntity};
use crate::entity::projectile::wither_skull::WitherSkullEntity;
use crate::entity::tnt::TNTEntity;
use crate::entity::vehicle::boat::BoatEntity;
use crate::entity::vehicle::minecart::MinecartEntity;
@@ -275,6 +277,7 @@ pub fn from_type(
}
id if id == EntityType::FIREBALL.id => Arc::new(FireballEntity::new(entity)),
id if id == EntityType::SMALL_FIREBALL.id => Arc::new(SmallFireballEntity::new(entity)),
id if id == EntityType::WITHER_SKULL.id => Arc::new(WitherSkullEntity::new(entity)),
id if id == EntityType::WIND_CHARGE.id => {
let thrown = ThrownItemEntity {
entity,
@@ -301,6 +304,7 @@ pub fn from_type(
id if id == EntityType::FIREWORK_ROCKET.id => Arc::new(FireworkRocketEntity::new(entity)),
id if id == EntityType::SPLASH_POTION.id => Arc::new(SplashPotionEntity::new(entity)),
id if id == EntityType::LINGERING_POTION.id => Arc::new(LingeringPotionEntity::new(entity)),
id if id == EntityType::LLAMA_SPIT.id => Arc::new(LlamaSpitEntity::new(entity)),
id if id == EntityType::EYE_OF_ENDER.id => Arc::new(EyeOfEnder::new(entity)),
id if id == EntityType::ACACIA_BOAT.id
|| id == EntityType::ACACIA_CHEST_BOAT.id

View File

@@ -1,8 +1,13 @@
use crate::entity::player::Player;
use crate::net::ClientPlatform;
use bitflags::bitflags;
use pumpkin_protocol::bedrock::client::boss_event::{BossEventAction, CBossEvent as BBossEvent};
use pumpkin_protocol::codec::var_int::VarInt;
use pumpkin_protocol::bedrock::client::boss_event::{
BOSS_EVENT_COLOUR_BLUE, BOSS_EVENT_COLOUR_GREEN, BOSS_EVENT_COLOUR_PINK,
BOSS_EVENT_COLOUR_PURPLE, BOSS_EVENT_COLOUR_RED, BOSS_EVENT_COLOUR_WHITE,
BOSS_EVENT_COLOUR_YELLOW, BOSS_EVENT_OVERLAY_NOTCHED_6, BOSS_EVENT_OVERLAY_NOTCHED_10,
BOSS_EVENT_OVERLAY_NOTCHED_12, BOSS_EVENT_OVERLAY_NOTCHED_20, BOSS_EVENT_OVERLAY_PROGRESS,
CBossEvent as BBossEvent,
};
use pumpkin_protocol::codec::var_long::VarLong;
use pumpkin_protocol::java::client::play::{BosseventAction, CBossEvent};
use pumpkin_util::text::TextComponent;
@@ -21,15 +26,15 @@ pub enum BossbarColor {
impl BossbarColor {
#[must_use]
pub const fn to_bedrock(self) -> VarInt {
pub const fn to_bedrock(self) -> u8 {
match self {
Self::Pink => VarInt(0),
Self::Blue => VarInt(1),
Self::Red => VarInt(2),
Self::Green => VarInt(3),
Self::Yellow => VarInt(4),
Self::Purple => VarInt(5),
Self::White => VarInt(6),
Self::Pink => BOSS_EVENT_COLOUR_PINK,
Self::Blue => BOSS_EVENT_COLOUR_BLUE,
Self::Red => BOSS_EVENT_COLOUR_RED,
Self::Green => BOSS_EVENT_COLOUR_GREEN,
Self::Yellow => BOSS_EVENT_COLOUR_YELLOW,
Self::Purple => BOSS_EVENT_COLOUR_PURPLE,
Self::White => BOSS_EVENT_COLOUR_WHITE,
}
}
}
@@ -45,13 +50,13 @@ pub enum BossbarDivisions {
impl BossbarDivisions {
#[must_use]
pub const fn to_bedrock(self) -> VarInt {
pub const fn to_bedrock(self) -> u8 {
match self {
Self::NoDivision => VarInt(0),
Self::Notches6 => VarInt(1),
Self::Notches10 => VarInt(2),
Self::Notches12 => VarInt(3),
Self::Notches20 => VarInt(4),
Self::NoDivision => BOSS_EVENT_OVERLAY_PROGRESS,
Self::Notches6 => BOSS_EVENT_OVERLAY_NOTCHED_6,
Self::Notches10 => BOSS_EVENT_OVERLAY_NOTCHED_10,
Self::Notches12 => BOSS_EVENT_OVERLAY_NOTCHED_12,
Self::Notches20 => BOSS_EVENT_OVERLAY_NOTCHED_20,
}
}
}
@@ -91,6 +96,14 @@ impl Bossbar {
}
}
#[inline]
#[must_use]
pub const fn bossbar_bedrock_id(uuid: &Uuid) -> VarLong {
let (high, low) = uuid.as_u64_pair();
let id = (high ^ low) & 0x7FFF_FFFF_FFFF_FFFF;
VarLong(id as i64)
}
/// Extra methods for [`Player`] to send and manage the bossbar.
impl Player {
pub async fn send_bossbar(&self, bossbar: &Bossbar) {
@@ -108,16 +121,20 @@ impl Player {
java.enqueue_client_packet(&packet).await;
}
ClientPlatform::Bedrock(bedrock) => {
let packet = BBossEvent {
boss_entity_id: VarLong(bossbar.uuid.as_u128() as i64),
action: BossEventAction::Add {
title: bossbar.title.clone().get_text(),
health_percent: bossbar.health,
color: bossbar.color.to_bedrock(),
overlay: bossbar.division.to_bedrock(),
},
};
let boss_id = bossbar_bedrock_id(&bossbar.uuid);
let player_id = VarLong(self.entity_id() as i64);
let packet = BBossEvent::show(
boss_id,
player_id,
bossbar.title.clone().get_text(),
bossbar.health,
bossbar.color.to_bedrock(),
bossbar.division.to_bedrock(),
);
bedrock.send_packet(&packet).await;
let register_packet = BBossEvent::register_player(boss_id, player_id);
bedrock.send_packet(&register_packet).await;
}
}
}
@@ -131,10 +148,12 @@ impl Player {
java.enqueue_client_packet(&packet).await;
}
ClientPlatform::Bedrock(bedrock) => {
let packet = BBossEvent {
boss_entity_id: VarLong(uuid.as_u128() as i64),
action: BossEventAction::Remove,
};
let boss_id = bossbar_bedrock_id(&uuid);
let player_id = VarLong(self.entity_id() as i64);
let unregister_packet = BBossEvent::unregister_player(boss_id, player_id);
bedrock.send_packet(&unregister_packet).await;
let packet = BBossEvent::hide(boss_id);
bedrock.send_packet(&packet).await;
}
}
@@ -149,10 +168,8 @@ impl Player {
java.enqueue_client_packet(&packet).await;
}
ClientPlatform::Bedrock(bedrock) => {
let packet = BBossEvent {
boss_entity_id: VarLong(uuid.as_u128() as i64),
action: BossEventAction::UpdateHealth(health),
};
let boss_id = bossbar_bedrock_id(uuid);
let packet = BBossEvent::update_health(boss_id, health);
bedrock.send_packet(&packet).await;
}
}
@@ -167,10 +184,8 @@ impl Player {
java.enqueue_client_packet(&packet).await;
}
ClientPlatform::Bedrock(bedrock) => {
let packet = BBossEvent {
boss_entity_id: VarLong(uuid.as_u128() as i64),
action: BossEventAction::UpdateTitle(title.get_text()),
};
let boss_id = bossbar_bedrock_id(uuid);
let packet = BBossEvent::update_title(boss_id, title.get_text());
bedrock.send_packet(&packet).await;
}
}
@@ -194,13 +209,12 @@ impl Player {
java.enqueue_client_packet(&packet).await;
}
ClientPlatform::Bedrock(bedrock) => {
let packet = BBossEvent {
boss_entity_id: VarLong(uuid.as_u128() as i64),
action: BossEventAction::UpdateProperties {
color: color.to_bedrock(),
overlay: dividers.to_bedrock(),
},
};
let boss_id = bossbar_bedrock_id(uuid);
let packet = BBossEvent::update_properties(
boss_id,
color.to_bedrock(),
dividers.to_bedrock(),
);
bedrock.send_packet(&packet).await;
}
}
@@ -215,18 +229,8 @@ impl Player {
java.enqueue_client_packet(&packet).await;
}
ClientPlatform::Bedrock(bedrock) => {
// For Bedrock, flags are part of properties (screen darken)
// We don't have color and dividers here, so we might need more info or just skip if not critical
// Actually, properties includes screen_darken, color, and overlay.
// Since this method only has flags, we might need to store the current color/division on the player
// or retrieve them from the bossbar if we had a reference to it.
let packet = BBossEvent {
boss_entity_id: VarLong(uuid.as_u128() as i64),
action: BossEventAction::UpdateProperties {
color: VarInt(0),
overlay: VarInt(0),
},
};
let boss_id = bossbar_bedrock_id(uuid);
let packet = BBossEvent::update_properties(boss_id, 0, 0);
bedrock.send_packet(&packet).await;
}
}

View File

@@ -64,7 +64,7 @@ impl CustomBossbars {
pub fn get_player_bars(&self, uuid: &Uuid) -> Option<Vec<&Bossbar>> {
let mut player_bars: Vec<&Bossbar> = Vec::new();
for bossbar in &self.custom_bossbars {
if bossbar.1.players.contains(uuid) {
if bossbar.1.visible && bossbar.1.players.contains(uuid) {
player_bars.push(&bossbar.1.bossbar_data);
}
}
@@ -141,6 +141,96 @@ impl CustomBossbars {
self.custom_bossbars.contains_key(resource_location)
}
pub async fn update_value(
&mut self,
server: &Server,
resource_location: String,
value: i32,
) -> Result<(), BossbarUpdateError> {
let bossbar = self.custom_bossbars.get_mut(&resource_location);
if let Some(bossbar) = bossbar {
if bossbar.value == value {
return Err(BossbarUpdateError::NoChanges("value", None));
}
let ratio = f64::from(value) / f64::from(bossbar.max);
let health: f32 = if ratio < 0.0 {
0.0
} else if ratio > 1.0 {
1.0
} else {
ratio as f32
};
bossbar.value = value;
bossbar.bossbar_data.health = health;
if !bossbar.visible {
return Ok(());
}
let players: Vec<Arc<Player>> = server.get_all_players();
let matching_players = players
.iter()
.filter(|player| bossbar.players.contains(&player.gameprofile.id));
for player in matching_players {
player
.update_bossbar_health(&bossbar.bossbar_data.uuid, bossbar.bossbar_data.health)
.await;
}
return Ok(());
}
Err(BossbarUpdateError::InvalidResourceLocation(
resource_location,
))
}
pub async fn update_max(
&mut self,
server: &Server,
resource_location: String,
max_value: i32,
) -> Result<(), BossbarUpdateError> {
let bossbar = self.custom_bossbars.get_mut(&resource_location);
if let Some(bossbar) = bossbar {
if bossbar.max == max_value {
return Err(BossbarUpdateError::NoChanges("max", None));
}
let ratio = f64::from(bossbar.value) / f64::from(max_value);
let health: f32 = if ratio < 0.0 {
0.0
} else if ratio > 1.0 {
1.0
} else {
ratio as f32
};
bossbar.max = max_value;
bossbar.bossbar_data.health = health;
if !bossbar.visible {
return Ok(());
}
let players: Vec<Arc<Player>> = server.get_all_players();
let matching_players = players
.iter()
.filter(|player| bossbar.players.contains(&player.gameprofile.id));
for player in matching_players {
player
.update_bossbar_health(&bossbar.bossbar_data.uuid, bossbar.bossbar_data.health)
.await;
}
return Ok(());
}
Err(BossbarUpdateError::InvalidResourceLocation(
resource_location,
))
}
pub async fn update_health(
&mut self,
server: &Server,

View File

@@ -24,6 +24,7 @@ pub mod explosion;
pub mod loot;
pub mod map;
pub mod portal;
pub mod raid;
pub mod time;
pub mod villager_poi;
@@ -274,6 +275,8 @@ pub struct World {
pub portal_poi: Mutex<portal::PortalPoiStorage>,
/// Villager job sites and their current owners.
pub villager_poi: Mutex<villager_poi::VillagerPoiStorage>,
/// Active raids in this world.
pub raids: Mutex<raid::Raids>,
/// End Dragon fight manager (only present in `THE_END` dimension).
pub dragon_fight: Option<Mutex<dragon_fight::DragonFight>>,
pub spawn_state: ArcSwap<SpawnState>,
@@ -393,6 +396,7 @@ impl World {
unsent_block_changes: Mutex::new(HashMap::new()),
portal_poi: Mutex::new(portal_poi),
villager_poi: Mutex::new(villager_poi::VillagerPoiStorage::default()),
raids: Mutex::new(raid::Raids::default()),
dragon_fight,
spawn_state: ArcSwap::new(Arc::new(SpawnState::empty())),
active_chunks: ArcSwap::new(Arc::new(FxHashSet::default())),
@@ -1203,6 +1207,7 @@ impl World {
self.flush_synced_block_events().await;
self.update_active_chunks();
self.tick_environment().await;
self.raids.lock().await.tick(self).await;
let world_for_chunks = self.clone();
let chunk_future = async move {
@@ -3668,11 +3673,17 @@ impl World {
.await;
}
// if let Some(bossbars) = self..lock().get_player_bars(&player.gameprofile.id) {
// for bossbar in bossbars {
// player.send_bossbar(bossbar);
// }
// }
let player_bossbars = server
.bossbars
.lock()
.await
.get_player_bars(&player.gameprofile.id)
.map(|bars| bars.into_iter().cloned().collect::<Vec<_>>());
if let Some(bossbars) = player_bossbars {
for bossbar in &bossbars {
player.send_bossbar(bossbar).await;
}
}
player.has_played_before.store(true, Ordering::Relaxed);
player

View File

@@ -0,0 +1,765 @@
use std::collections::{HashMap, HashSet};
use std::sync::Arc;
use uuid::Uuid;
use pumpkin_data::data_component_impl::EquipmentSlot;
use pumpkin_data::effect::StatusEffect;
use pumpkin_data::entity::EntityType;
use pumpkin_data::potion::Effect;
use pumpkin_data::sound::Sound;
use pumpkin_util::difficulty::Difficulty;
use pumpkin_util::math::position::BlockPos;
use pumpkin_util::math::vector3::Vector3;
use pumpkin_util::text::TextComponent;
use crate::entity::EntityBase;
use crate::entity::mob::raider::create_ominous_banner;
use crate::entity::player::Player;
use crate::entity::r#type::from_type;
use crate::world::World;
use crate::world::bossbar::{Bossbar, BossbarColor, BossbarDivisions};
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum RaidStatus {
Ongoing,
Victory,
Loss,
Stopped,
}
impl RaidStatus {
#[must_use]
pub const fn get_serialized_name(self) -> &'static str {
match self {
Self::Ongoing => "ongoing",
Self::Victory => "victory",
Self::Loss => "loss",
Self::Stopped => "stopped",
}
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum RaiderType {
Vindicator,
Evoker,
Pillager,
Witch,
Ravager,
}
impl RaiderType {
pub const VALUES: [Self; 5] = [
Self::Vindicator,
Self::Evoker,
Self::Pillager,
Self::Witch,
Self::Ravager,
];
#[must_use]
pub const fn entity_type(self) -> &'static EntityType {
match self {
Self::Vindicator => &EntityType::VINDICATOR,
Self::Evoker => &EntityType::EVOKER,
Self::Pillager => &EntityType::PILLAGER,
Self::Witch => &EntityType::WITCH,
Self::Ravager => &EntityType::RAVAGER,
}
}
#[must_use]
pub const fn spawns_per_wave(self) -> &'static [i32; 8] {
match self {
Self::Vindicator => &[0, 0, 2, 0, 1, 4, 2, 5],
Self::Evoker => &[0, 0, 0, 0, 0, 1, 1, 2],
Self::Pillager => &[0, 4, 3, 3, 4, 4, 4, 2],
Self::Witch => &[0, 0, 0, 0, 3, 0, 0, 1],
Self::Ravager => &[0, 0, 0, 1, 0, 1, 0, 2],
}
}
}
pub struct Raid {
pub id: i32,
pub center: BlockPos,
pub status: RaidStatus,
pub active: bool,
pub started: bool,
pub ticks_active: u64,
pub raid_omen_level: i32,
pub groups_spawned: i32,
pub num_groups: i32,
pub raid_cooldown_ticks: i32,
pub post_raid_ticks: i32,
pub celebration_ticks: u32,
pub total_health: f32,
pub bossbar: Bossbar,
pub group_to_leader_map: HashMap<i32, Uuid>,
pub group_raider_map: HashMap<i32, HashSet<Uuid>>,
pub heroes_of_the_village: HashSet<Uuid>,
pub players_in_raid: HashSet<Uuid>,
pub wave_spawn_pos: Option<BlockPos>,
}
impl Raid {
pub const VALID_RAID_RADIUS: f64 = 96.0;
pub const VALID_RAID_RADIUS_SQR: f64 = 9216.0;
pub const RAID_REMOVAL_THRESHOLD_SQR: f64 = 12544.0;
pub const MAX_RAID_OMEN_LEVEL: i32 = 5;
#[must_use]
pub fn new(id: i32, center: BlockPos, difficulty: Difficulty) -> Self {
let mut bossbar = Bossbar::new(TextComponent::translate("event.minecraft.raid", []));
bossbar.color = BossbarColor::Red;
bossbar.division = BossbarDivisions::Notches10;
bossbar.health = 0.0;
let num_groups = Self::get_num_groups(difficulty);
Self {
id,
center,
status: RaidStatus::Ongoing,
active: true,
started: false,
ticks_active: 0,
raid_omen_level: 1,
groups_spawned: 0,
num_groups,
raid_cooldown_ticks: 300,
post_raid_ticks: 0,
celebration_ticks: 0,
total_health: 0.0,
bossbar,
group_to_leader_map: HashMap::new(),
group_raider_map: HashMap::new(),
heroes_of_the_village: HashSet::new(),
players_in_raid: HashSet::new(),
wave_spawn_pos: None,
}
}
#[must_use]
pub const fn get_num_groups(difficulty: Difficulty) -> i32 {
match difficulty {
Difficulty::Peaceful => 0,
Difficulty::Easy => 3,
Difficulty::Normal => 5,
Difficulty::Hard => 7,
}
}
#[must_use]
pub const fn is_over(&self) -> bool {
self.is_victory() || self.is_loss()
}
#[must_use]
pub const fn is_stopped(&self) -> bool {
matches!(self.status, RaidStatus::Stopped)
}
#[must_use]
pub const fn is_victory(&self) -> bool {
matches!(self.status, RaidStatus::Victory)
}
#[must_use]
pub const fn is_loss(&self) -> bool {
matches!(self.status, RaidStatus::Loss)
}
#[must_use]
pub const fn is_started(&self) -> bool {
self.started
}
#[must_use]
pub const fn is_active(&self) -> bool {
self.active
}
#[must_use]
pub const fn get_groups_spawned(&self) -> i32 {
self.groups_spawned
}
#[must_use]
pub const fn get_raid_omen_level(&self) -> i32 {
self.raid_omen_level
}
pub fn set_raid_omen_level(&mut self, level: i32) {
self.raid_omen_level = level.clamp(0, Self::MAX_RAID_OMEN_LEVEL);
}
#[must_use]
pub const fn has_more_waves(&self) -> bool {
if self.has_bonus_wave() {
!self.has_spawned_bonus_wave()
} else {
!self.is_final_wave()
}
}
#[must_use]
pub const fn is_final_wave(&self) -> bool {
self.groups_spawned == self.num_groups
}
#[must_use]
pub const fn has_bonus_wave(&self) -> bool {
self.raid_omen_level > 1
}
#[must_use]
pub const fn has_spawned_bonus_wave(&self) -> bool {
self.groups_spawned > self.num_groups
}
#[must_use]
pub fn should_spawn_bonus_group(&self) -> bool {
self.is_final_wave() && self.get_total_raiders_alive() == 0 && self.has_bonus_wave()
}
#[must_use]
pub fn should_spawn_group(&self) -> bool {
self.raid_cooldown_ticks == 0
&& (self.groups_spawned < self.num_groups || self.should_spawn_bonus_group())
&& self.get_total_raiders_alive() == 0
}
#[must_use]
pub fn get_total_raiders_alive(&self) -> usize {
self.group_raider_map.values().map(HashSet::len).sum()
}
#[must_use]
pub fn get_all_raiders(&self) -> HashSet<Uuid> {
let mut set = HashSet::new();
for wave_set in self.group_raider_map.values() {
set.extend(wave_set);
}
set
}
pub async fn stop(&mut self, world: &World) {
self.active = false;
self.status = RaidStatus::Stopped;
self.remove_all_players(world).await;
}
pub async fn remove_all_players(&mut self, world: &World) {
let players = world.players.load();
for player in players.iter() {
if self.players_in_raid.contains(&player.gameprofile.id) {
player.remove_bossbar(self.bossbar.uuid).await;
}
}
self.players_in_raid.clear();
}
pub fn absorb_raid_omen(&mut self, _player: &Player) -> bool {
self.raid_omen_level = (self.raid_omen_level + 1).clamp(1, Self::MAX_RAID_OMEN_LEVEL);
true
}
pub fn add_hero_of_the_village(&mut self, killer_uuid: Uuid) {
self.heroes_of_the_village.insert(killer_uuid);
}
const fn get_default_num_spawns(
&self,
raider_type: RaiderType,
wave: i32,
is_bonus_wave: bool,
) -> i32 {
let spawns = raider_type.spawns_per_wave();
if is_bonus_wave {
spawns[self.num_groups as usize]
} else {
spawns[wave as usize]
}
}
fn get_potential_bonus_spawns(
raider_type: RaiderType,
wave: i32,
difficulty: Difficulty,
is_bonus_wave: bool,
) -> i32 {
let is_easy = difficulty == Difficulty::Easy;
let is_normal = difficulty == Difficulty::Normal;
let bonus_spawns = match raider_type {
RaiderType::Vindicator | RaiderType::Pillager => {
if is_easy {
rand::random::<u32>() % 2
} else if is_normal {
1
} else {
2
}
}
RaiderType::Evoker => 0,
RaiderType::Witch => u32::from(!(is_easy || wave <= 2 || wave == 4)),
RaiderType::Ravager => u32::from(!is_easy && is_bonus_wave),
};
if bonus_spawns > 0 {
(rand::random::<u32>() % (bonus_spawns + 1)) as i32
} else {
0
}
}
pub async fn spawn_group(&mut self, world: &Arc<World>, pos: BlockPos) {
let mut leader_set = false;
let group_number = self.groups_spawned + 1;
self.total_health = 0.0;
let is_bonus_group = self.should_spawn_bonus_group();
let difficulty = world.level_info.load().difficulty;
for raider_type in RaiderType::VALUES {
let num_spawns = self.get_default_num_spawns(raider_type, group_number, is_bonus_group)
+ Self::get_potential_bonus_spawns(
raider_type,
group_number,
difficulty,
is_bonus_group,
);
let mut ravagers_spawned = 0;
for _ in 0..num_spawns {
let uuid = Uuid::new_v4();
let spawn_pos = Vector3::new(
f64::from(pos.0.x) + 0.5,
f64::from(pos.0.y) + 1.0,
f64::from(pos.0.z) + 0.5,
);
let entity_base = from_type(raider_type.entity_type(), spawn_pos, world, uuid);
if let Some(mob) = entity_base.get_mob()
&& let Some(raider) = mob.as_raider()
{
if !leader_set && raider.can_be_leader() {
raider.set_patrol_leader(true);
let banner = create_ominous_banner();
let living = &mob.get_mob_entity().living_entity;
let mut equipment = living.entity_equipment.lock().await;
equipment.put(&EquipmentSlot::HEAD, banner.clone());
drop(equipment);
living.send_equipment_changes(&[(EquipmentSlot::HEAD, banner)]);
self.group_to_leader_map.insert(group_number, uuid);
leader_set = true;
}
raider.set_wave(group_number);
raider.set_can_join_raid(true);
raider.apply_raid_buffs(group_number, false);
}
self.join_raid(group_number, uuid, &entity_base);
world.spawn_entity(entity_base.clone()).await;
if *raider_type.entity_type() == EntityType::RAVAGER {
let mut riding_type: Option<&'static EntityType> = None;
if group_number == Self::get_num_groups(Difficulty::Normal) {
riding_type = Some(&EntityType::PILLAGER);
} else if group_number >= Self::get_num_groups(Difficulty::Hard) {
if ravagers_spawned == 0 {
riding_type = Some(&EntityType::EVOKER);
} else {
riding_type = Some(&EntityType::VINDICATOR);
}
}
ravagers_spawned += 1;
if let Some(riding_type) = riding_type {
let rider_uuid = Uuid::new_v4();
let rider_base = from_type(riding_type, spawn_pos, world, rider_uuid);
if let Some(mob) = rider_base.get_mob()
&& let Some(raider) = mob.as_raider()
{
raider.set_wave(group_number);
raider.set_can_join_raid(true);
raider.apply_raid_buffs(group_number, false);
}
self.join_raid(group_number, rider_uuid, &rider_base);
world.spawn_entity(rider_base).await;
}
}
}
}
self.wave_spawn_pos = None;
self.groups_spawned += 1;
self.update_bossbar(world).await;
}
pub fn join_raid(&mut self, wave: i32, uuid: Uuid, entity_base: &Arc<dyn EntityBase>) {
self.group_raider_map.entry(wave).or_default().insert(uuid);
if let Some(living) = entity_base.get_living_entity() {
self.total_health += living.health.load();
}
}
#[must_use]
pub fn find_random_spawn_pos(&self, _world: &World, max_tries: usize) -> Option<BlockPos> {
let seconds_remaining = self.raid_cooldown_ticks / 20;
let how_far = 0.22 * (seconds_remaining as f32) - 0.24;
let start_angle = rand::random::<f32>() * std::f32::consts::PI * 2.0;
for i in 0..max_tries {
let angle = start_angle + std::f32::consts::PI * (i as f32) / 8.0;
let spawn_x = self.center.0.x
+ ((angle.cos() * 32.0 * how_far) as i32)
+ (rand::random::<i32>().rem_euclid(3)) * (how_far as i32);
let spawn_z = self.center.0.z
+ ((angle.sin() * 32.0 * how_far) as i32)
+ (rand::random::<i32>().rem_euclid(3)) * (how_far as i32);
let spawn_y = self.center.0.y;
if (spawn_y - self.center.0.y).abs() <= 96 {
let spawn_pos = BlockPos(Vector3::new(spawn_x, spawn_y, spawn_z));
return Some(spawn_pos);
}
}
None
}
pub fn play_sound(&self, world: &World, sound_origin: BlockPos) {
let raid_loc = sound_origin.to_f64();
let players = world.players.load();
for player in players.iter() {
let player_loc = player.get_entity().pos.load();
let dx = raid_loc.x - player_loc.x;
let dz = raid_loc.z - player_loc.z;
let dist = dx.hypot(dz);
let sound_pos = if dist > 0.001 {
Vector3::new(
player_loc.x + (13.0 / dist) * dx,
player_loc.y,
player_loc.z + (13.0 / dist) * dz,
)
} else {
player_loc
};
if dist <= 64.0 || self.players_in_raid.contains(&player.gameprofile.id) {
world.play_sound(
Sound::EventRaidHorn,
pumpkin_data::sound::SoundCategory::Neutral,
&sound_pos,
);
}
}
}
pub async fn update_players(&mut self, world: &World) {
let center_f64 = self.center.to_f64();
let players = world.players.load();
let mut current_nearby = HashSet::new();
for player in players.iter() {
let pos = player.get_entity().pos.load();
let dist_sq = pos.squared_distance_to_vec(&center_f64);
if dist_sq <= Self::VALID_RAID_RADIUS_SQR && player.living_entity.health.load() > 0.0 {
current_nearby.insert(player.gameprofile.id);
if self.players_in_raid.insert(player.gameprofile.id) {
player.send_bossbar(&self.bossbar).await;
}
}
}
let mut to_remove = Vec::new();
for player_uuid in &self.players_in_raid {
if !current_nearby.contains(player_uuid) {
to_remove.push(*player_uuid);
}
}
for player_uuid in to_remove {
self.players_in_raid.remove(&player_uuid);
if let Some(player) = players.iter().find(|p| p.gameprofile.id == player_uuid) {
player.remove_bossbar(self.bossbar.uuid).await;
}
}
}
pub fn update_raiders(&mut self, world: &World) {
let center_f64 = self.center.to_f64();
for raiders in self.group_raider_map.values_mut() {
let mut wave_dead = Vec::new();
for &raider_uuid in raiders.iter() {
let entity = world.get_entity_by_uuid(raider_uuid);
match entity {
Some(e) => {
let is_dead = e.get_living_entity().is_none_or(|l| l.health.load() <= 0.0);
let pos = e.get_entity().pos.load();
let dist_sq = pos.squared_distance_to_vec(&center_f64);
if is_dead || dist_sq >= Self::RAID_REMOVAL_THRESHOLD_SQR {
wave_dead.push(raider_uuid);
}
}
None => {
wave_dead.push(raider_uuid);
}
}
}
for uuid in wave_dead {
raiders.remove(&uuid);
}
}
}
pub async fn update_bossbar(&mut self, world: &World) {
let living_health = self.get_health_of_living_raiders(world);
let progress = if self.total_health > 0.0 {
(living_health / self.total_health).clamp(0.0, 1.0)
} else {
0.0
};
self.bossbar.health = progress;
let players = world.players.load();
for player in players.iter() {
if self.players_in_raid.contains(&player.gameprofile.id) {
player
.update_bossbar_health(&self.bossbar.uuid, self.bossbar.health)
.await;
player
.update_bossbar_title(&self.bossbar.uuid, self.bossbar.title.clone())
.await;
}
}
}
#[must_use]
pub fn get_health_of_living_raiders(&self, world: &World) -> f32 {
let mut health = 0.0;
for raiders in self.group_raider_map.values() {
for &uuid in raiders {
if let Some(e) = world.get_entity_by_uuid(uuid)
&& let Some(living) = e.get_living_entity()
{
health += living.health.load();
}
}
}
health
}
pub async fn tick(&mut self, world: &Arc<World>) {
if self.is_stopped() {
return;
}
if self.status == RaidStatus::Ongoing {
if world.level_info.load().difficulty == Difficulty::Peaceful {
self.stop(world).await;
return;
}
self.ticks_active += 1;
if self.ticks_active >= 48000 {
self.stop(world).await;
return;
}
let raiders_alive = self.get_total_raiders_alive();
if raiders_alive == 0 && self.has_more_waves() {
if self.raid_cooldown_ticks <= 0 {
if self.groups_spawned > 0 {
self.raid_cooldown_ticks = 300;
self.bossbar.title = TextComponent::translate("event.minecraft.raid", []);
}
} else {
if self.wave_spawn_pos.is_none() && self.raid_cooldown_ticks % 5 == 0 {
self.wave_spawn_pos = self.find_random_spawn_pos(world, 8);
}
if self.raid_cooldown_ticks == 300 || self.raid_cooldown_ticks % 20 == 0 {
self.update_players(world).await;
}
self.raid_cooldown_ticks -= 1;
self.bossbar.health =
((300 - self.raid_cooldown_ticks) as f32 / 300.0).clamp(0.0, 1.0);
}
}
if self.ticks_active.is_multiple_of(20) {
self.update_players(world).await;
self.update_raiders(world);
let alive = self.get_total_raiders_alive();
if alive > 0 && alive <= 2 {
self.bossbar.title = TextComponent::translate(
"event.minecraft.raid.raiders_remaining",
[TextComponent::text(alive.to_string())],
);
} else {
self.bossbar.title = TextComponent::translate("event.minecraft.raid", []);
}
self.update_bossbar(world).await;
}
while self.should_spawn_group() {
let spawn_pos = self
.wave_spawn_pos
.or_else(|| self.find_random_spawn_pos(world, 20))
.unwrap_or(self.center);
self.started = true;
self.spawn_group(world, spawn_pos).await;
self.play_sound(world, spawn_pos);
}
let raiders_alive = self.get_total_raiders_alive();
if self.is_started() && !self.has_more_waves() && raiders_alive == 0 {
if self.post_raid_ticks < 40 {
self.post_raid_ticks += 1;
} else {
self.status = RaidStatus::Victory;
let effect = Effect {
effect_type: &StatusEffect::HERO_OF_THE_VILLAGE,
duration: 48000,
amplifier: (self.raid_omen_level - 1).max(0) as u8,
ambient: false,
show_particles: false,
show_icon: true,
blend: true,
};
let players = world.players.load();
for player in players.iter() {
if self.heroes_of_the_village.contains(&player.gameprofile.id) {
player.add_effect(effect.clone()).await;
}
}
}
}
} else if self.is_over() {
self.celebration_ticks += 1;
if self.celebration_ticks >= 600 {
self.stop(world).await;
return;
}
if self.celebration_ticks.is_multiple_of(20) {
self.update_players(world).await;
if self.is_victory() {
self.bossbar.health = 0.0;
self.bossbar.title =
TextComponent::translate("event.minecraft.raid.victory.full", []);
} else {
self.bossbar.title =
TextComponent::translate("event.minecraft.raid.defeat.full", []);
}
self.update_bossbar(world).await;
}
}
}
}
#[derive(Default)]
pub struct Raids {
pub raid_map: HashMap<i32, Raid>,
pub next_id: i32,
pub tick_counter: u64,
}
impl Raids {
#[must_use]
pub fn get(&self, raid_id: i32) -> Option<&Raid> {
self.raid_map.get(&raid_id)
}
pub fn get_mut(&mut self, raid_id: i32) -> Option<&mut Raid> {
self.raid_map.get_mut(&raid_id)
}
#[must_use]
pub fn get_raid_at(&self, pos: &BlockPos) -> Option<&Raid> {
let pos_f64 = pos.to_f64();
self.raid_map.values().find(|r| {
r.is_active()
&& r.center.to_f64().squared_distance_to_vec(&pos_f64)
<= Raid::VALID_RAID_RADIUS_SQR
})
}
pub fn get_raid_at_mut(&mut self, pos: &BlockPos) -> Option<&mut Raid> {
let pos_f64 = pos.to_f64();
self.raid_map.values_mut().find(|r| {
r.is_active()
&& r.center.to_f64().squared_distance_to_vec(&pos_f64)
<= Raid::VALID_RAID_RADIUS_SQR
})
}
#[must_use]
pub fn get_nearby_raid(&self, pos: &BlockPos, max_dist_sqr: f64) -> Option<&Raid> {
let pos_f64 = pos.to_f64();
let mut closest = None;
let mut closest_dist = max_dist_sqr;
for raid in self.raid_map.values() {
let dist = raid.center.to_f64().squared_distance_to_vec(&pos_f64);
if raid.is_active() && dist < closest_dist {
closest = Some(raid);
closest_dist = dist;
}
}
closest
}
pub fn create_or_extend_raid(
&mut self,
player: &Player,
raid_position: BlockPos,
world: &Arc<World>,
) -> Option<i32> {
let raid_center_pos = raid_position;
let existing_id = self.raid_map.iter().find_map(|(&id, r)| {
let dist = r
.center
.to_f64()
.squared_distance_to_vec(&raid_center_pos.to_f64());
(r.is_active() && dist <= Raid::VALID_RAID_RADIUS_SQR).then_some(id)
});
if let Some(id) = existing_id {
if let Some(raid) = self.raid_map.get_mut(&id) {
raid.absorb_raid_omen(player);
}
Some(id)
} else {
self.next_id += 1;
let id = self.next_id;
let mut raid = Raid::new(id, raid_center_pos, world.level_info.load().difficulty);
raid.absorb_raid_omen(player);
self.raid_map.insert(id, raid);
Some(id)
}
}
pub async fn tick(&mut self, world: &Arc<World>) {
self.tick_counter += 1;
let mut stopped_ids = Vec::new();
for (&id, raid) in &mut self.raid_map {
raid.tick(world).await;
if raid.is_stopped() {
stopped_ids.push(id);
}
}
for id in stopped_ids {
if let Some(mut raid) = self.raid_map.remove(&id) {
raid.remove_all_players(world).await;
}
}
}
}

View File

@@ -102,6 +102,24 @@ impl VillagerPoiStorage {
sites.sort_unstable_by_key(|(distance, _)| *distance);
sites.into_iter().map(|(_, position)| position).collect()
}
#[must_use]
pub fn get_nearest_job_site(&self, origin: BlockPos, radius: i32) -> Option<BlockPos> {
let radius_squared = i64::from(radius).pow(2);
let mut closest = None;
let mut closest_dist = radius_squared;
for position in self.job_sites.keys() {
let delta = position.0 - origin.0;
let distance_squared =
i64::from(delta.x).pow(2) + i64::from(delta.y).pow(2) + i64::from(delta.z).pow(2);
if distance_squared <= closest_dist {
closest = Some(*position);
closest_dist = distance_squared;
}
}
closest
}
}
#[must_use]