mirror of
https://github.com/Pumpkin-MC/Pumpkin.git
synced 2026-08-30 20:14:23 +00:00
feat: add pillager raids
This commit is contained in:
@@ -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,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -88,6 +88,7 @@ impl BlockBehaviour for WitherSkeletonSkullBlock {
|
||||
&EntityType::WITHER,
|
||||
);
|
||||
let wither = WitherEntity::new(entity);
|
||||
wither.make_invulnerable();
|
||||
world.spawn_entity(wither).await;
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -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),
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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");
|
||||
|
||||
339
crates/pumpkin/src/command/commands/raid.rs
Normal file
339
crates/pumpkin/src/command/commands/raid.rs
Normal 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))
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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;
|
||||
|
||||
204
crates/pumpkin/src/entity/ai/goal/pathfind_to_raid.rs
Normal file
204
crates/pumpkin/src/entity/ai/goal/pathfind_to_raid.rs
Normal 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,
|
||||
});
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
178
crates/pumpkin/src/entity/ai/goal/ranged_attack.rs
Normal file
178
crates/pumpkin/src/entity/ai/goal/ranged_attack.rs
Normal 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;
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -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 ¤t {
|
||||
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);
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
29
crates/pumpkin/src/entity/effect/hunger.rs
Normal file
29
crates/pumpkin/src/entity/effect/hunger.rs
Normal 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);
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
73
crates/pumpkin/src/entity/effect/infested.rs
Normal file
73
crates/pumpkin/src/entity/effect/infested.rs
Normal 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, ¢er);
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -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(®ENERATION)
|
||||
} 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 {
|
||||
|
||||
47
crates/pumpkin/src/entity/effect/oozing.rs
Normal file
47
crates/pumpkin/src/entity/effect/oozing.rs
Normal 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;
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
44
crates/pumpkin/src/entity/effect/poison.rs
Normal file
44
crates/pumpkin/src/entity/effect/poison.rs
Normal 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;
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
32
crates/pumpkin/src/entity/effect/raid_omen.rs
Normal file
32
crates/pumpkin/src/entity/effect/raid_omen.rs
Normal 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();
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
32
crates/pumpkin/src/entity/effect/regeneration.rs
Normal file
32
crates/pumpkin/src/entity/effect/regeneration.rs
Normal 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);
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
29
crates/pumpkin/src/entity/effect/saturation.rs
Normal file
29
crates/pumpkin/src/entity/effect/saturation.rs
Normal 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);
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
69
crates/pumpkin/src/entity/effect/weaving.rs
Normal file
69
crates/pumpkin/src/entity/effect/weaving.rs
Normal 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);
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
44
crates/pumpkin/src/entity/effect/wind_charged.rs
Normal file
44
crates/pumpkin/src/entity/effect/wind_charged.rs
Normal 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,
|
||||
¢er,
|
||||
);
|
||||
})
|
||||
}
|
||||
}
|
||||
37
crates/pumpkin/src/entity/effect/wither.rs
Normal file
37
crates/pumpkin/src/entity/effect/wither.rs
Normal 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;
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -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, ¢er);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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(
|
||||
|
||||
@@ -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>,
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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, ()>;
|
||||
}
|
||||
|
||||
227
crates/pumpkin/src/entity/mob/patrol.rs
Normal file
227
crates/pumpkin/src/entity/mob/patrol.rs
Normal 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,
|
||||
});
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
418
crates/pumpkin/src/entity/mob/raider.rs
Normal file
418
crates/pumpkin/src/entity/mob/raider.rs
Normal 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;
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
@@ -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() {
|
||||
|
||||
114
crates/pumpkin/src/entity/projectile/llama_spit.rs
Normal file
114
crates/pumpkin/src/entity/projectile/llama_spit.rs
Normal 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;
|
||||
});
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -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 {
|
||||
|
||||
176
crates/pumpkin/src/entity/projectile/wither_skull.rs
Normal file
176
crates/pumpkin/src/entity/projectile/wither_skull.rs
Normal 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;
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
|
||||
@@ -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(®ister_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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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
|
||||
|
||||
765
crates/pumpkin/src/world/raid.rs
Normal file
765
crates/pumpkin/src/world/raid.rs
Normal 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(¢er_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(¢er_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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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]
|
||||
|
||||
Reference in New Issue
Block a user