From 09d6eb6da2850e619081d6c5d90370d33274a3be Mon Sep 17 00:00:00 2001 From: drakeerv <60192521+drakeerv@users.noreply.github.com> Date: Sun, 2 Feb 2025 05:22:49 -0500 Subject: [PATCH] Add damage types & damage command (#526) --- assets/damage_type.json | 1 + pumpkin-data/build/build.rs | 2 + pumpkin-data/build/damage_type.rs | 141 ++++++++++++++++++ pumpkin-data/src/lib.rs | 4 + pumpkin/src/command/args/damage_type.rs | 74 ++++++++++ pumpkin/src/command/args/mod.rs | 3 + pumpkin/src/command/commands/damage.rs | 188 ++++++++++++++++++++++++ pumpkin/src/command/commands/mod.rs | 1 + pumpkin/src/command/mod.rs | 7 +- pumpkin/src/entity/living.rs | 34 ++++- pumpkin/src/entity/mod.rs | 13 ++ pumpkin/src/entity/player.rs | 10 +- pumpkin/src/server/mod.rs | 1 + 13 files changed, 467 insertions(+), 12 deletions(-) create mode 100644 assets/damage_type.json create mode 100644 pumpkin-data/build/damage_type.rs create mode 100644 pumpkin/src/command/args/damage_type.rs create mode 100644 pumpkin/src/command/commands/damage.rs diff --git a/assets/damage_type.json b/assets/damage_type.json new file mode 100644 index 000000000..ec88c8083 --- /dev/null +++ b/assets/damage_type.json @@ -0,0 +1 @@ +{"arrow":{"id":0,"components":{"exhaustion":0.1,"message_id":"arrow","scaling":"when_caused_by_living_non_player"}},"bad_respawn_point":{"id":1,"components":{"exhaustion":0.1,"death_message_type":"intentional_game_design","message_id":"badRespawnPoint","scaling":"always"}},"cactus":{"id":2,"components":{"exhaustion":0.1,"message_id":"cactus","scaling":"when_caused_by_living_non_player"}},"campfire":{"id":3,"components":{"exhaustion":0.1,"effects":"burning","message_id":"inFire","scaling":"when_caused_by_living_non_player"}},"cramming":{"id":4,"components":{"exhaustion":0.0,"message_id":"cramming","scaling":"when_caused_by_living_non_player"}},"dragon_breath":{"id":5,"components":{"exhaustion":0.0,"message_id":"dragonBreath","scaling":"when_caused_by_living_non_player"}},"drown":{"id":6,"components":{"exhaustion":0.0,"effects":"drowning","message_id":"drown","scaling":"when_caused_by_living_non_player"}},"dry_out":{"id":7,"components":{"exhaustion":0.1,"message_id":"dryout","scaling":"when_caused_by_living_non_player"}},"ender_pearl":{"id":8,"components":{"exhaustion":0.0,"death_message_type":"fall_variants","message_id":"fall","scaling":"when_caused_by_living_non_player"}},"explosion":{"id":9,"components":{"exhaustion":0.1,"message_id":"explosion","scaling":"always"}},"fall":{"id":10,"components":{"exhaustion":0.0,"death_message_type":"fall_variants","message_id":"fall","scaling":"when_caused_by_living_non_player"}},"falling_anvil":{"id":11,"components":{"exhaustion":0.1,"message_id":"anvil","scaling":"when_caused_by_living_non_player"}},"falling_block":{"id":12,"components":{"exhaustion":0.1,"message_id":"fallingBlock","scaling":"when_caused_by_living_non_player"}},"falling_stalactite":{"id":13,"components":{"exhaustion":0.1,"message_id":"fallingStalactite","scaling":"when_caused_by_living_non_player"}},"fireball":{"id":14,"components":{"exhaustion":0.1,"effects":"burning","message_id":"fireball","scaling":"when_caused_by_living_non_player"}},"fireworks":{"id":15,"components":{"exhaustion":0.1,"message_id":"fireworks","scaling":"when_caused_by_living_non_player"}},"fly_into_wall":{"id":16,"components":{"exhaustion":0.0,"message_id":"flyIntoWall","scaling":"when_caused_by_living_non_player"}},"freeze":{"id":17,"components":{"exhaustion":0.0,"effects":"freezing","message_id":"freeze","scaling":"when_caused_by_living_non_player"}},"generic":{"id":18,"components":{"exhaustion":0.0,"message_id":"generic","scaling":"when_caused_by_living_non_player"}},"generic_kill":{"id":19,"components":{"exhaustion":0.0,"message_id":"genericKill","scaling":"when_caused_by_living_non_player"}},"hot_floor":{"id":20,"components":{"exhaustion":0.1,"effects":"burning","message_id":"hotFloor","scaling":"when_caused_by_living_non_player"}},"in_fire":{"id":21,"components":{"exhaustion":0.1,"effects":"burning","message_id":"inFire","scaling":"when_caused_by_living_non_player"}},"in_wall":{"id":22,"components":{"exhaustion":0.0,"message_id":"inWall","scaling":"when_caused_by_living_non_player"}},"indirect_magic":{"id":23,"components":{"exhaustion":0.0,"message_id":"indirectMagic","scaling":"when_caused_by_living_non_player"}},"lava":{"id":24,"components":{"exhaustion":0.1,"effects":"burning","message_id":"lava","scaling":"when_caused_by_living_non_player"}},"lightning_bolt":{"id":25,"components":{"exhaustion":0.1,"message_id":"lightningBolt","scaling":"when_caused_by_living_non_player"}},"mace_smash":{"id":26,"components":{"exhaustion":0.1,"message_id":"mace_smash","scaling":"when_caused_by_living_non_player"}},"magic":{"id":27,"components":{"exhaustion":0.0,"message_id":"magic","scaling":"when_caused_by_living_non_player"}},"mob_attack":{"id":28,"components":{"exhaustion":0.1,"message_id":"mob","scaling":"when_caused_by_living_non_player"}},"mob_attack_no_aggro":{"id":29,"components":{"exhaustion":0.1,"message_id":"mob","scaling":"when_caused_by_living_non_player"}},"mob_projectile":{"id":30,"components":{"exhaustion":0.1,"message_id":"mob","scaling":"when_caused_by_living_non_player"}},"on_fire":{"id":31,"components":{"exhaustion":0.0,"effects":"burning","message_id":"onFire","scaling":"when_caused_by_living_non_player"}},"out_of_world":{"id":32,"components":{"exhaustion":0.0,"message_id":"outOfWorld","scaling":"when_caused_by_living_non_player"}},"outside_border":{"id":33,"components":{"exhaustion":0.0,"message_id":"outsideBorder","scaling":"when_caused_by_living_non_player"}},"player_attack":{"id":34,"components":{"exhaustion":0.1,"message_id":"player","scaling":"when_caused_by_living_non_player"}},"player_explosion":{"id":35,"components":{"exhaustion":0.1,"message_id":"explosion.player","scaling":"always"}},"sonic_boom":{"id":36,"components":{"exhaustion":0.0,"message_id":"sonic_boom","scaling":"always"}},"spit":{"id":37,"components":{"exhaustion":0.1,"message_id":"mob","scaling":"when_caused_by_living_non_player"}},"stalagmite":{"id":38,"components":{"exhaustion":0.0,"message_id":"stalagmite","scaling":"when_caused_by_living_non_player"}},"starve":{"id":39,"components":{"exhaustion":0.0,"message_id":"starve","scaling":"when_caused_by_living_non_player"}},"sting":{"id":40,"components":{"exhaustion":0.1,"message_id":"sting","scaling":"when_caused_by_living_non_player"}},"sweet_berry_bush":{"id":41,"components":{"exhaustion":0.1,"effects":"poking","message_id":"sweetBerryBush","scaling":"when_caused_by_living_non_player"}},"thorns":{"id":42,"components":{"exhaustion":0.1,"effects":"thorns","message_id":"thorns","scaling":"when_caused_by_living_non_player"}},"thrown":{"id":43,"components":{"exhaustion":0.1,"message_id":"thrown","scaling":"when_caused_by_living_non_player"}},"trident":{"id":44,"components":{"exhaustion":0.1,"message_id":"trident","scaling":"when_caused_by_living_non_player"}},"unattributed_fireball":{"id":45,"components":{"exhaustion":0.1,"effects":"burning","message_id":"onFire","scaling":"when_caused_by_living_non_player"}},"wind_charge":{"id":46,"components":{"exhaustion":0.1,"message_id":"mob","scaling":"when_caused_by_living_non_player"}},"wither":{"id":47,"components":{"exhaustion":0.0,"message_id":"wither","scaling":"when_caused_by_living_non_player"}},"wither_skull":{"id":48,"components":{"exhaustion":0.1,"message_id":"witherSkull","scaling":"when_caused_by_living_non_player"}}} \ No newline at end of file diff --git a/pumpkin-data/build/build.rs b/pumpkin-data/build/build.rs index f5cb9c321..5f30a2c07 100644 --- a/pumpkin-data/build/build.rs +++ b/pumpkin-data/build/build.rs @@ -7,6 +7,7 @@ use syn::Ident; mod biome; mod chunk_status; +mod damage_type; mod entity_pose; mod entity_type; mod game_event; @@ -34,6 +35,7 @@ pub fn main() { write_generated_file(entity_type::build(), "entity_type.rs"); write_generated_file(noise_parameter::build(), "noise_parameter.rs"); write_generated_file(biome::build(), "biome.rs"); + write_generated_file(damage_type::build(), "damage_type.rs"); write_generated_file(message_type::build(), "message_type.rs"); } diff --git a/pumpkin-data/build/damage_type.rs b/pumpkin-data/build/damage_type.rs new file mode 100644 index 000000000..910eec5c5 --- /dev/null +++ b/pumpkin-data/build/damage_type.rs @@ -0,0 +1,141 @@ +use heck::{ToPascalCase, ToShoutySnakeCase}; +use proc_macro2::TokenStream; +use quote::quote; +use serde::Deserialize; +use std::collections::HashMap; + +#[derive(Deserialize)] +struct DamageTypeEntry { + id: u32, + components: DamageTypeData, +} + +#[derive(Deserialize)] +pub struct DamageTypeData { + death_message_type: Option, + exhaustion: f32, + message_id: String, + scaling: String, +} + +pub(crate) fn build() -> TokenStream { + println!("cargo:rerun-if-changed=../assets/damage_type.json"); + + let damage_types: HashMap = + serde_json::from_str(include_str!("../../assets/damage_type.json")) + .expect("Failed to parse damage_type.json"); + + let mut constants = Vec::new(); + let mut enum_variants = Vec::new(); + + for (name, entry) in damage_types { + let const_ident = crate::ident(name.to_shouty_snake_case()); + let enum_ident = crate::ident(name.to_pascal_case()); + + enum_variants.push(enum_ident.clone()); + + let data = &entry.components; + let death_message_type = match &data.death_message_type { + Some(msg) => quote! { Some(#msg) }, + None => quote! { None }, + }; + + let exhaustion = data.exhaustion; + let message_id = &data.message_id; + let scaling = &data.scaling; + let id = entry.id; + + constants.push(quote! { + pub const #const_ident: DamageTypeData = DamageTypeData { + death_message_type: #death_message_type, + exhaustion: #exhaustion, + message_id: #message_id, + scaling: #scaling, + id: #id, + }; + }); + } + + let enum_arms = enum_variants.iter().map(|variant| { + let const_name = variant.to_string().to_shouty_snake_case(); + let const_ident = crate::ident(&const_name); + quote! { + DamageType::#variant => &#const_ident, + } + }); + + let type_name_pairs = enum_variants.iter().map(|variant| { + let name = variant.to_string(); + let name_lowercase = name.to_lowercase(); + let resource_name = format!("minecraft:{}", name_lowercase); + quote! { + #resource_name => Some(Self::#variant) + } + }); + + let type_to_name_pairs = enum_variants.iter().map(|variant| { + let name = variant.to_string(); + let name_lowercase = name.to_lowercase(); + let resource_name = format!("minecraft:{}", name_lowercase); + quote! { + Self::#variant => #resource_name + } + }); + + // Create array of all variants for values() method + let variant_array = enum_variants.iter().map(|variant| { + quote! { + DamageType::#variant + } + }); + + quote! { + #[derive(Clone, Debug)] + pub struct DamageTypeData { + pub death_message_type: Option<&'static str>, + pub exhaustion: f32, + pub message_id: &'static str, + pub scaling: &'static str, + pub id: u32, + } + + #(#constants)* + + #[derive(Clone, Copy, Debug, PartialEq, Eq)] + #[repr(u8)] + pub enum DamageType { + #(#enum_variants,)* + } + + impl DamageType { + pub const fn data(&self) -> &'static DamageTypeData { + match self { + #(#enum_arms)* + } + } + + #[doc = r" Get all possible damage types"] + pub fn values() -> &'static [DamageType] { + static VALUES: &[DamageType] = &[ + #(#variant_array,)* + ]; + VALUES + } + + #[doc = r" Try to parse a damage type from a resource location string"] + pub fn from_name(name: &str) -> Option { + match name { + #(#type_name_pairs,)* + _ => None + } + } + + #[doc = r" Get the resource location string for this damage type"] + pub const fn to_name(&self) -> &'static str { + match self { + #(#type_to_name_pairs,)* + } + } + } + } +} diff --git a/pumpkin-data/src/lib.rs b/pumpkin-data/src/lib.rs index 6cca51aa5..3ea2ad79e 100644 --- a/pumpkin-data/src/lib.rs +++ b/pumpkin-data/src/lib.rs @@ -38,3 +38,7 @@ pub mod world { pub mod scoreboard { include!(concat!(env!("OUT_DIR"), "/scoreboard_slot.rs")); } + +pub mod damage { + include!(concat!(env!("OUT_DIR"), "/damage_type.rs")); +} diff --git a/pumpkin/src/command/args/damage_type.rs b/pumpkin/src/command/args/damage_type.rs new file mode 100644 index 000000000..fdebe7df1 --- /dev/null +++ b/pumpkin/src/command/args/damage_type.rs @@ -0,0 +1,74 @@ +use async_trait::async_trait; +use pumpkin_data::damage::DamageType; +use pumpkin_protocol::client::play::{ArgumentType, CommandSuggestion, SuggestionProviders}; + +use crate::command::{ + args::{Arg, ArgumentConsumer, DefaultNameArgConsumer, FindArg, GetClientSideArgParser}, + dispatcher::CommandError, + tree::RawArgs, + CommandSender, +}; +use crate::server::Server; + +pub struct DamageTypeArgumentConsumer; + +impl GetClientSideArgParser for DamageTypeArgumentConsumer { + fn get_client_side_parser(&self) -> ArgumentType { + ArgumentType::ResourceLocation + } + + fn get_client_side_suggestion_type_override(&self) -> Option { + Some(SuggestionProviders::AskServer) + } +} + +#[async_trait] +impl ArgumentConsumer for DamageTypeArgumentConsumer { + async fn consume<'a>( + &'a self, + _sender: &CommandSender<'a>, + _server: &'a Server, + args: &mut RawArgs<'a>, + ) -> Option> { + let s = args.pop()?; + + // Create a static damage type first + let damage_type = DamageType::from_name(s)?; + // Find matching static damage type from values array + DamageType::values() + .iter() + .find(|&&dt| std::mem::discriminant(&dt) == std::mem::discriminant(&damage_type)) + .map(Arg::DamageType) + } + + async fn suggest<'a>( + &'a self, + _sender: &CommandSender<'a>, + _server: &'a Server, + _input: &'a str, + ) -> Result>, CommandError> { + // Get all available damage types + let suggestions = DamageType::values() + .iter() + .map(|dt| CommandSuggestion::new(dt.to_name().to_string(), None)) + .collect(); + Ok(Some(suggestions)) + } +} + +impl DefaultNameArgConsumer for DamageTypeArgumentConsumer { + fn default_name(&self) -> &'static str { + "damageType" + } +} + +impl<'a> FindArg<'a> for DamageTypeArgumentConsumer { + type Data = &'a DamageType; + + fn find_arg(args: &'a super::ConsumedArgs, name: &str) -> Result { + match args.get(name) { + Some(Arg::DamageType(data)) => Ok(data), + _ => Err(CommandError::InvalidConsumption(Some(name.to_string()))), + } + } +} diff --git a/pumpkin/src/command/args/mod.rs b/pumpkin/src/command/args/mod.rs index f364e7079..53934fdc3 100644 --- a/pumpkin/src/command/args/mod.rs +++ b/pumpkin/src/command/args/mod.rs @@ -2,6 +2,7 @@ use std::{collections::HashMap, hash::Hash, sync::Arc}; use async_trait::async_trait; use bounded_num::{NotInBounds, Number}; +use pumpkin_data::damage::DamageType; use pumpkin_data::sound::SoundCategory; use pumpkin_protocol::client::play::{ArgumentType, CommandSuggestion, SuggestionProviders}; use pumpkin_util::text::TextComponent; @@ -25,6 +26,7 @@ pub mod bossbar_style; pub mod bounded_num; pub mod command; mod coordinate; +pub mod damage_type; pub mod entities; pub mod entity; pub mod gamemode; @@ -97,6 +99,7 @@ pub enum Arg<'a> { #[allow(unused)] Simple(&'a str), SoundCategory(SoundCategory), + DamageType(&'a DamageType), } /// see [`crate::commands::tree::builder::argument`] and [`CommandTree::execute`]/[`crate::commands::tree::builder::NonLeafNodeBuilder::execute`] diff --git a/pumpkin/src/command/commands/damage.rs b/pumpkin/src/command/commands/damage.rs new file mode 100644 index 000000000..2dc572f30 --- /dev/null +++ b/pumpkin/src/command/commands/damage.rs @@ -0,0 +1,188 @@ +use async_trait::async_trait; +use pumpkin_data::damage::DamageType; +use pumpkin_util::text::{ + color::{Color, NamedColor}, + TextComponent, +}; + +use crate::command::{ + args::{ + bounded_num::BoundedNumArgumentConsumer, damage_type::DamageTypeArgumentConsumer, + entity::EntityArgumentConsumer, position_3d::Position3DArgumentConsumer, Arg, ConsumedArgs, + FindArg, + }, + tree::builder::{argument, literal}, + tree::CommandTree, + CommandError, CommandExecutor, CommandSender, +}; + +const NAMES: [&str; 1] = ["damage"]; +const DESCRIPTION: &str = "Deals damage to entities"; +const ARG_TARGET: &str = "target"; +const ARG_AMOUNT: &str = "amount"; +const ARG_DAMAGE_TYPE: &str = "damageType"; +const ARG_LOCATION: &str = "location"; +const ARG_ENTITY: &str = "entity"; +const ARG_CAUSE: &str = "cause"; + +fn amount_consumer() -> BoundedNumArgumentConsumer { + BoundedNumArgumentConsumer::new().name(ARG_AMOUNT).min(0.0) +} + +struct DamageLocationExecutor; +struct DamageEntityExecutor(bool); + +async fn send_damage_result( + sender: &mut CommandSender<'_>, + success: bool, + amount: f32, + target_name: String, +) { + if !success { + sender + .send_message( + TextComponent::translate("commands.damage.invulnerable", [].into()) + .color(Color::Named(NamedColor::Red)), + ) + .await; + return; + } + + sender + .send_message(TextComponent::translate( + "commands.damage.success", + [ + TextComponent::text(amount.to_string()), + TextComponent::text(target_name), + ] + .into(), + )) + .await; +} + +#[async_trait] +impl CommandExecutor for DamageLocationExecutor { + async fn execute<'a>( + &self, + sender: &mut CommandSender<'a>, + _server: &crate::server::Server, + args: &ConsumedArgs<'a>, + ) -> Result<(), CommandError> { + let target = EntityArgumentConsumer::find_arg(args, ARG_TARGET)?; + + let Ok(Ok(amount)) = BoundedNumArgumentConsumer::::find_arg(args, ARG_AMOUNT) else { + sender + .send_message( + TextComponent::text("Invalid damage amount") + .color(Color::Named(NamedColor::Red)), + ) + .await; + return Ok(()); + }; + + let damage_type = args + .get(ARG_DAMAGE_TYPE) + .map_or(DamageType::Generic, |arg| match arg { + Arg::DamageType(dt) => **dt, + _ => DamageType::Generic, + }); + + let location = Position3DArgumentConsumer::find_arg(args, ARG_LOCATION)?; + + let success = target + .living_entity + .damage_with_context(amount, damage_type, Some(location), None, None) + .await; + + send_damage_result(sender, success, amount, target.gameprofile.name.clone()).await; + + Ok(()) + } +} + +#[async_trait] +impl CommandExecutor for DamageEntityExecutor { + async fn execute<'a>( + &self, + sender: &mut CommandSender<'a>, + _server: &crate::server::Server, + args: &ConsumedArgs<'a>, + ) -> Result<(), CommandError> { + let target = EntityArgumentConsumer::find_arg(args, ARG_TARGET)?; + + let Ok(Ok(amount)) = BoundedNumArgumentConsumer::::find_arg(args, ARG_AMOUNT) else { + sender + .send_message( + TextComponent::text("Invalid damage amount") + .color(Color::Named(NamedColor::Red)), + ) + .await; + return Ok(()); + }; + + let damage_type = args + .get(ARG_DAMAGE_TYPE) + .map_or(DamageType::Generic, |arg| match arg { + Arg::DamageType(dt) => **dt, + _ => DamageType::Generic, + }); + + let source = EntityArgumentConsumer::find_arg(args, ARG_ENTITY).ok(); + let cause = if self.0 { + EntityArgumentConsumer::find_arg(args, ARG_CAUSE).ok() + } else { + None + }; + + let success = target + .living_entity + .damage_with_context( + amount, + damage_type, + None, + source.as_ref().map(|e| &e.living_entity.entity), + cause.as_ref().map(|e| &e.living_entity.entity), + ) + .await; + + send_damage_result(sender, success, amount, target.gameprofile.name.clone()).await; + + Ok(()) + } +} + +pub fn init_command_tree() -> CommandTree { + CommandTree::new(NAMES, DESCRIPTION).then( + argument(ARG_TARGET, EntityArgumentConsumer).then( + argument(ARG_AMOUNT, amount_consumer()) + // Basic damage + .execute(DamageEntityExecutor(false)) + // With damage type + .then( + argument(ARG_DAMAGE_TYPE, DamageTypeArgumentConsumer) + .execute(DamageEntityExecutor(false)) + // At location + .then( + literal("at").then( + argument(ARG_LOCATION, Position3DArgumentConsumer) + .execute(DamageLocationExecutor), + ), + ) + // By entity + .then( + literal("by").then( + argument(ARG_ENTITY, EntityArgumentConsumer) + .execute(DamageEntityExecutor(false)) + // From cause + .then( + literal("from").then( + argument(ARG_CAUSE, EntityArgumentConsumer) + .execute(DamageEntityExecutor(true)), + ), + ), + ), + ), + ), + ), + ) +} diff --git a/pumpkin/src/command/commands/mod.rs b/pumpkin/src/command/commands/mod.rs index 0c1cf6e45..0ca14ba97 100644 --- a/pumpkin/src/command/commands/mod.rs +++ b/pumpkin/src/command/commands/mod.rs @@ -3,6 +3,7 @@ pub mod banip; pub mod banlist; pub mod bossbar; pub mod clear; +pub mod damage; pub mod deop; pub mod experience; pub mod fill; diff --git a/pumpkin/src/command/mod.rs b/pumpkin/src/command/mod.rs index 52379be2b..00fce75c6 100644 --- a/pumpkin/src/command/mod.rs +++ b/pumpkin/src/command/mod.rs @@ -10,9 +10,9 @@ use crate::world::World; use args::ConsumedArgs; use async_trait::async_trait; use commands::{ - ban, banip, banlist, clear, deop, experience, fill, gamemode, give, help, kick, kill, list, me, - msg, op, pardon, pardonip, playsound, plugin, plugins, pumpkin, say, setblock, stop, summon, - teleport, time, title, worldborder, + ban, banip, banlist, clear, damage, deop, experience, fill, gamemode, give, help, kick, kill, + list, me, msg, op, pardon, pardonip, playsound, plugin, plugins, pumpkin, say, setblock, stop, + summon, teleport, time, title, worldborder, }; use dispatcher::CommandError; use pumpkin_util::math::vector3::Vector3; @@ -143,6 +143,7 @@ pub fn default_dispatcher() -> CommandDispatcher { dispatcher.register(pardon::init_command_tree(), PermissionLvl::Three); dispatcher.register(pardonip::init_command_tree(), PermissionLvl::Three); dispatcher.register(experience::init_command_tree(), PermissionLvl::Two); + dispatcher.register(damage::init_command_tree(), PermissionLvl::Two); dispatcher } diff --git a/pumpkin/src/entity/living.rs b/pumpkin/src/entity/living.rs index 444e0b0e8..c7c1f3b0c 100644 --- a/pumpkin/src/entity/living.rs +++ b/pumpkin/src/entity/living.rs @@ -2,7 +2,7 @@ use std::sync::atomic::AtomicI32; use async_trait::async_trait; use crossbeam::atomic::AtomicCell; -use pumpkin_data::sound::Sound; +use pumpkin_data::{damage::DamageType, sound::Sound}; use pumpkin_nbt::tag::NbtTag; use pumpkin_protocol::client::play::{CDamageEvent, CEntityStatus, MetaDataType, Metadata}; use pumpkin_util::math::vector3::Vector3; @@ -66,16 +66,27 @@ impl LivingEntity { self.entity.entity_id } - // TODO add damage_type enum - pub async fn damage(&self, amount: f32, damage_type: u8) { + pub async fn damage_with_context( + &self, + amount: f32, + damage_type: DamageType, + position: Option>, + source: Option<&Entity>, + cause: Option<&Entity>, + ) -> bool { + // Check invulnerability before applying damage + if self.entity.is_invulnerable_to(damage_type) { + return false; + } + self.entity .world .broadcast_packet_all(&CDamageEvent::new( self.entity.entity_id.into(), - damage_type.into(), - None, - None, - None, + damage_type.data().id.into(), + source.map(|e| e.entity_id.into()), + cause.map(|e| e.entity_id.into()), + position, )) .await; @@ -86,6 +97,13 @@ impl LivingEntity { } else { self.set_health(new_health).await; } + + true + } + + pub async fn damage(&self, amount: f32, damage_type: DamageType) -> bool { + self.damage_with_context(amount, damage_type, None, None, None) + .await } /// Returns if the entity was damaged or not @@ -132,7 +150,7 @@ impl LivingEntity { .play_sound(Self::get_fall_sound(fall_distance as i32)) .await; // TODO: Play block fall sound - self.damage(damage, 10).await; // Fall + self.damage(damage, DamageType::Fall).await; // Fall } else if height_difference < 0.0 { let distance = self.fall_distance.load(); self.fall_distance diff --git a/pumpkin/src/entity/mod.rs b/pumpkin/src/entity/mod.rs index 8037f6891..0cfa67388 100644 --- a/pumpkin/src/entity/mod.rs +++ b/pumpkin/src/entity/mod.rs @@ -9,6 +9,7 @@ use crossbeam::atomic::AtomicCell; use living::LivingEntity; use player::Player; use pumpkin_data::{ + damage::DamageType, entity::{EntityPose, EntityType}, sound::{Sound, SoundCategory}, }; @@ -91,6 +92,10 @@ pub struct Entity { pub bounding_box: AtomicCell, ///The size (width and height) of the bounding box pub bounding_box_size: AtomicCell, + /// Whether this entity is invulnerable to all damage + pub invulnerable: AtomicBool, + /// List of damage types this entity is immune to + pub damage_immunities: Vec, } impl Entity { @@ -104,6 +109,7 @@ impl Entity { standing_eye_height: f32, bounding_box: AtomicCell, bounding_box_size: AtomicCell, + invulnerable: bool, ) -> Self { let floor_x = position.x.floor() as i32; let floor_y = position.y.floor() as i32; @@ -130,6 +136,8 @@ impl Entity { pose: AtomicCell::new(EntityPose::Standing), bounding_box, bounding_box_size, + invulnerable: AtomicBool::new(invulnerable), + damage_immunities: Vec::new(), } } @@ -352,6 +360,11 @@ impl Entity { self.send_meta_data(Metadata::new(6, MetaDataType::EntityPose, VarInt(pose))) .await; } + + pub fn is_invulnerable_to(&self, damage_type: DamageType) -> bool { + self.invulnerable.load(std::sync::atomic::Ordering::Relaxed) + || self.damage_immunities.contains(&damage_type) + } } #[async_trait] diff --git a/pumpkin/src/entity/player.rs b/pumpkin/src/entity/player.rs index 438c039c9..4fc2c9494 100644 --- a/pumpkin/src/entity/player.rs +++ b/pumpkin/src/entity/player.rs @@ -11,6 +11,7 @@ use async_trait::async_trait; use crossbeam::atomic::AtomicCell; use pumpkin_config::{ADVANCED_CONFIG, BASIC_CONFIG}; use pumpkin_data::{ + damage::DamageType, entity::EntityType, sound::{Sound, SoundCategory}, }; @@ -183,6 +184,7 @@ impl Player { 1.62, AtomicCell::new(BoundingBox::new_default(&bounding_box_size)), AtomicCell::new(bounding_box_size), + matches!(gamemode, GameMode::Creative | GameMode::Spectator), )), config: Mutex::new(config), gameprofile, @@ -354,7 +356,7 @@ impl Player { victim .living_entity - .damage(damage as f32, 34) // PlayerAttack + .damage(damage as f32, DamageType::PlayerAttack) // PlayerAttack .await; let mut knockback_strength = 1.0; @@ -675,6 +677,11 @@ impl Player { abilities.set_for_gamemode(gamemode); }; self.send_abilities_update().await; + + self.living_entity.entity.invulnerable.store( + matches!(gamemode, GameMode::Creative | GameMode::Spectator), + std::sync::atomic::Ordering::Relaxed, + ); self.living_entity .entity .world @@ -686,6 +693,7 @@ impl Player { }], )) .await; + #[allow(clippy::cast_precision_loss)] self.client .send_packet(&CGameEvent::new( diff --git a/pumpkin/src/server/mod.rs b/pumpkin/src/server/mod.rs index 223daa96e..79cfa8bcc 100644 --- a/pumpkin/src/server/mod.rs +++ b/pumpkin/src/server/mod.rs @@ -252,6 +252,7 @@ impl Server { &bounding_box_size, )), AtomicCell::new(bounding_box_size), + false, ) }