feat: Implement Trident
This commit is contained in:
parent
e0ec4f9569
commit
c52531b37a
@ -918,20 +918,57 @@ impl Player {
|
||||
let mut damage_multiplier = 1.0;
|
||||
let mut add_damage = 0.0;
|
||||
let mut add_speed = 0.0;
|
||||
let mut extra_ench_damage = 0.0;
|
||||
let mut knockback_level = 0u32;
|
||||
|
||||
// Get the attack damage from the held item
|
||||
// TODO: this should be cached in memory, we shouldn't just use default here either
|
||||
if let Some(modifiers) = item_stack
|
||||
.lock()
|
||||
.await
|
||||
.get_data_component::<AttributeModifiersImpl>()
|
||||
{
|
||||
for item_mod in modifiers.attribute_modifiers.iter() {
|
||||
if item_mod.operation == Operation::AddValue {
|
||||
if item_mod.id == "minecraft:base_attack_damage" {
|
||||
add_damage = item_mod.amount;
|
||||
} else if item_mod.id == "minecraft:base_attack_speed" {
|
||||
add_speed = item_mod.amount;
|
||||
let stack = item_stack.lock().await;
|
||||
if let Some(modifiers) = stack.get_data_component::<AttributeModifiersImpl>() {
|
||||
for item_mod in modifiers.attribute_modifiers.iter() {
|
||||
if item_mod.operation == Operation::AddValue {
|
||||
if item_mod.id == "minecraft:base_attack_damage" {
|
||||
add_damage = item_mod.amount;
|
||||
} else if item_mod.id == "minecraft:base_attack_speed" {
|
||||
add_speed = item_mod.amount;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if let Some(enchantments) = stack.get_data_component::<EnchantmentsImpl>() {
|
||||
for (enchantment, level) in enchantments.enchantment.iter() {
|
||||
if **enchantment == Enchantment::SHARPNESS {
|
||||
extra_ench_damage += 0.5 * f64::from(*level) + 0.5;
|
||||
} else if **enchantment == Enchantment::SMITE {
|
||||
let target_type = victim_entity.entity_type.id;
|
||||
let is_undead = target_type == EntityType::ZOMBIE.id
|
||||
|| target_type == EntityType::DROWNED.id
|
||||
|| target_type == EntityType::HUSK.id
|
||||
|| target_type == EntityType::ZOMBIE_VILLAGER.id
|
||||
|| target_type == EntityType::ZOMBIFIED_PIGLIN.id
|
||||
|| target_type == EntityType::SKELETON.id
|
||||
|| target_type == EntityType::BOGGED.id
|
||||
|| target_type == EntityType::PARCHED.id
|
||||
|| target_type == EntityType::WITHER_SKELETON.id
|
||||
|| target_type == EntityType::STRAY.id
|
||||
|| target_type == EntityType::PHANTOM.id
|
||||
|| target_type == EntityType::WITHER.id
|
||||
|| target_type == EntityType::ZOMBIE_HORSE.id
|
||||
|| target_type == EntityType::SKELETON_HORSE.id;
|
||||
if is_undead {
|
||||
extra_ench_damage += 2.5 * f64::from(*level);
|
||||
}
|
||||
} else if **enchantment == Enchantment::BANE_OF_ARTHROPODS {
|
||||
let target_type = victim_entity.entity_type.id;
|
||||
let is_arthropod = target_type == EntityType::SPIDER.id
|
||||
|| target_type == EntityType::CAVE_SPIDER.id
|
||||
|| target_type == EntityType::SILVERFISH.id
|
||||
|| target_type == EntityType::ENDERMITE.id
|
||||
|| target_type == EntityType::BEE.id;
|
||||
if is_arthropod {
|
||||
extra_ench_damage += 2.5 * f64::from(*level);
|
||||
}
|
||||
} else if **enchantment == Enchantment::KNOCKBACK {
|
||||
knockback_level = *level as u32;
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -954,6 +991,7 @@ impl Player {
|
||||
|
||||
// Modify the added damage based on the multiplier.
|
||||
let mut damage = base_damage + add_damage * damage_multiplier;
|
||||
damage += extra_ench_damage * attack_cooldown_progress;
|
||||
|
||||
if let Some(strength) = self
|
||||
.living_entity
|
||||
@ -1048,7 +1086,7 @@ impl Player {
|
||||
);
|
||||
|
||||
if victim.get_living_entity().is_some() {
|
||||
let mut knockback_strength = 1.0;
|
||||
let mut knockback_strength = 1.0 + f64::from(knockback_level);
|
||||
match attack_type {
|
||||
AttackType::Knockback => knockback_strength += 1.0,
|
||||
AttackType::Sweeping => {
|
||||
|
||||
@ -55,6 +55,8 @@ pub struct ArrowEntity {
|
||||
pub pickup: ArrowPickup,
|
||||
pub is_critical: AtomicBool,
|
||||
pub pierce_level: AtomicU8,
|
||||
pub punch_level: AtomicU8,
|
||||
pub is_flame: AtomicBool,
|
||||
pub in_ground: AtomicBool,
|
||||
pub in_ground_time: AtomicU32,
|
||||
pub life: AtomicU32,
|
||||
@ -78,6 +80,8 @@ impl ArrowEntity {
|
||||
pickup: ArrowPickup::Disallowed,
|
||||
is_critical: AtomicBool::new(false),
|
||||
pierce_level: AtomicU8::new(0),
|
||||
punch_level: AtomicU8::new(0),
|
||||
is_flame: AtomicBool::new(false),
|
||||
in_ground: AtomicBool::new(false),
|
||||
in_ground_time: AtomicU32::new(0),
|
||||
life: AtomicU32::new(0),
|
||||
@ -100,6 +104,8 @@ impl ArrowEntity {
|
||||
pickup,
|
||||
is_critical: AtomicBool::new(false),
|
||||
pierce_level: AtomicU8::new(0),
|
||||
punch_level: AtomicU8::new(0),
|
||||
is_flame: AtomicBool::new(false),
|
||||
in_ground: AtomicBool::new(false),
|
||||
in_ground_time: AtomicU32::new(0),
|
||||
life: AtomicU32::new(0),
|
||||
@ -390,12 +396,27 @@ impl EntityBase for ArrowEntity {
|
||||
let bonus = (rand::random::<u32>() % (damage / 2 + 2) as u32) as i32;
|
||||
damage = damage.saturating_add(bonus);
|
||||
}
|
||||
if self.is_flame.load(Ordering::Relaxed) {
|
||||
target.get_entity().set_on_fire_for_ticks(100);
|
||||
}
|
||||
|
||||
target
|
||||
.damage(&*target, damage as f32, DamageType::ARROW)
|
||||
.await;
|
||||
|
||||
if target.get_living_entity().is_some() {
|
||||
// TODO: knockback
|
||||
let punch = self.punch_level.load(Ordering::Relaxed);
|
||||
if punch > 0 {
|
||||
if let Some(owner_id) = self.owner_id {
|
||||
if let Some(owner_entity) = world.get_entity_by_id(owner_id) {
|
||||
crate::entity::combat::handle_knockback(
|
||||
owner_entity.get_entity(),
|
||||
target.get_entity(),
|
||||
f64::from(punch) * 0.6,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Play hit sound
|
||||
let sound_packet = CSoundEffect::new(
|
||||
|
||||
@ -21,11 +21,13 @@ pub mod shulker_bullet;
|
||||
pub mod small_fireball;
|
||||
pub mod snowball;
|
||||
pub mod splash_potion;
|
||||
pub mod trident;
|
||||
pub mod wind_charge;
|
||||
|
||||
#[must_use]
|
||||
pub fn is_projectile(entity_type: &EntityType) -> bool {
|
||||
*entity_type == EntityType::ARROW
|
||||
|| *entity_type == EntityType::TRIDENT
|
||||
|| *entity_type == EntityType::EGG
|
||||
|| *entity_type == EntityType::SNOWBALL
|
||||
|| *entity_type == EntityType::FIREWORK_ROCKET
|
||||
|
||||
460
pumpkin/src/entity/projectile/trident.rs
Normal file
460
pumpkin/src/entity/projectile/trident.rs
Normal file
@ -0,0 +1,460 @@
|
||||
use std::sync::Arc;
|
||||
use std::sync::atomic::{AtomicBool, AtomicU8, AtomicU32, Ordering};
|
||||
use tokio::sync::Mutex;
|
||||
|
||||
use crate::{
|
||||
entity::{
|
||||
Entity, EntityBase, EntityBaseFuture, NBTStorage, living::LivingEntity, player::Player,
|
||||
},
|
||||
server::Server,
|
||||
};
|
||||
use pumpkin_data::damage::DamageType;
|
||||
use pumpkin_data::item::Item;
|
||||
use pumpkin_data::item_stack::ItemStack;
|
||||
use pumpkin_data::sound::{Sound, SoundCategory};
|
||||
use pumpkin_protocol::IdOr;
|
||||
use pumpkin_protocol::java::client::play::{CEntityVelocity, CSoundEffect};
|
||||
use pumpkin_util::math::boundingbox::BoundingBox;
|
||||
use pumpkin_util::math::position::BlockPos;
|
||||
use pumpkin_util::math::vector3::Vector3;
|
||||
|
||||
use super::ProjectileHit;
|
||||
use super::arrow::ArrowPickup;
|
||||
|
||||
pub struct TridentEntity {
|
||||
pub entity: Entity,
|
||||
pub owner_id: Option<i32>,
|
||||
pub item_stack: Arc<Mutex<ItemStack>>,
|
||||
pub pickup: ArrowPickup,
|
||||
pub in_ground: AtomicBool,
|
||||
pub in_ground_time: AtomicU32,
|
||||
pub life: AtomicU32,
|
||||
pub shake_time: AtomicU8,
|
||||
pub has_hit: AtomicBool,
|
||||
pub last_block_pos: Arc<std::sync::RwLock<Option<BlockPos>>>,
|
||||
}
|
||||
|
||||
impl TridentEntity {
|
||||
const BASE_DAMAGE: f64 = 8.0;
|
||||
const AIR_INERTIA: f64 = 0.99;
|
||||
const WATER_INERTIA: f64 = 0.9;
|
||||
const GRAVITY: f64 = 0.05;
|
||||
const DESPAWN_TIME: u32 = 1200;
|
||||
|
||||
pub fn new(entity: Entity, owner_id: Option<i32>) -> Self {
|
||||
Self {
|
||||
entity,
|
||||
owner_id,
|
||||
item_stack: Arc::new(Mutex::new(ItemStack::new(1, &Item::TRIDENT))),
|
||||
pickup: ArrowPickup::Disallowed,
|
||||
in_ground: AtomicBool::new(false),
|
||||
in_ground_time: AtomicU32::new(0),
|
||||
life: AtomicU32::new(0),
|
||||
shake_time: AtomicU8::new(0),
|
||||
has_hit: AtomicBool::new(false),
|
||||
last_block_pos: Arc::new(std::sync::RwLock::new(None)),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn new_shot(
|
||||
entity: Entity,
|
||||
shooter: &Entity,
|
||||
item_stack: ItemStack,
|
||||
pickup: ArrowPickup,
|
||||
) -> Self {
|
||||
let mut owner_pos = shooter.pos.load();
|
||||
owner_pos.y = owner_pos.y + f64::from(shooter.entity_dimension.load().eye_height) - 0.1;
|
||||
entity.pos.store(owner_pos);
|
||||
entity.set_velocity(Vector3::new(0.0, 0.1, 0.0));
|
||||
|
||||
Self {
|
||||
entity,
|
||||
owner_id: Some(shooter.entity_id),
|
||||
item_stack: Arc::new(Mutex::new(item_stack)),
|
||||
pickup,
|
||||
in_ground: AtomicBool::new(false),
|
||||
in_ground_time: AtomicU32::new(0),
|
||||
life: AtomicU32::new(0),
|
||||
shake_time: AtomicU8::new(0),
|
||||
has_hit: AtomicBool::new(false),
|
||||
last_block_pos: Arc::new(std::sync::RwLock::new(None)),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn set_velocity_from_rotation(
|
||||
&self,
|
||||
pitch: f32,
|
||||
yaw: f32,
|
||||
roll: f32,
|
||||
speed: f32,
|
||||
divergence: f32,
|
||||
) {
|
||||
let yaw_rad = yaw.to_radians();
|
||||
let pitch_rad = pitch.to_radians();
|
||||
let roll_rad = (pitch + roll).to_radians();
|
||||
|
||||
let x = -yaw_rad.sin() * pitch_rad.cos();
|
||||
let y = -roll_rad.sin();
|
||||
let z = yaw_rad.cos() * pitch_rad.cos();
|
||||
|
||||
self.set_velocity(
|
||||
f64::from(x),
|
||||
f64::from(y),
|
||||
f64::from(z),
|
||||
f64::from(speed),
|
||||
f64::from(divergence),
|
||||
);
|
||||
}
|
||||
|
||||
pub fn set_velocity(&self, x: f64, y: f64, z: f64, power: f64, uncertainty: f64) {
|
||||
fn next_triangular(mode: f64, deviation: f64) -> f64 {
|
||||
deviation.mul_add(rand::random::<f64>() - rand::random::<f64>(), mode)
|
||||
}
|
||||
|
||||
let velocity = Vector3::new(x, y, z)
|
||||
.normalize()
|
||||
.add_raw(
|
||||
next_triangular(0.0, 0.017_227_5 * uncertainty),
|
||||
next_triangular(0.0, 0.017_227_5 * uncertainty),
|
||||
next_triangular(0.0, 0.017_227_5 * uncertainty),
|
||||
)
|
||||
.multiply(power, power, power);
|
||||
|
||||
self.entity.velocity.store(velocity);
|
||||
let len = velocity.horizontal_length();
|
||||
self.entity.set_rotation(
|
||||
velocity.x.atan2(velocity.z) as f32 * 57.295_776,
|
||||
velocity.y.atan2(len) as f32 * 57.295_776,
|
||||
);
|
||||
}
|
||||
|
||||
fn should_skip_collision(&self, self_ent: &Entity, other: &Arc<dyn EntityBase>) -> bool {
|
||||
let other_ent = other.get_entity();
|
||||
|
||||
// Don't collide with self
|
||||
if other_ent.entity_id == self_ent.entity_id {
|
||||
return true;
|
||||
}
|
||||
|
||||
// Skip owner for initial frames (5 ticks)
|
||||
if Some(other_ent.entity_id) == self.owner_id && self_ent.age.load(Ordering::Relaxed) < 5 {
|
||||
return true;
|
||||
}
|
||||
|
||||
// Skip other projectiles and item entities
|
||||
if other_ent.entity_type == &pumpkin_data::entity::EntityType::ARROW
|
||||
|| other_ent.entity_type == &pumpkin_data::entity::EntityType::TRIDENT
|
||||
|| other_ent.entity_type == &pumpkin_data::entity::EntityType::ITEM
|
||||
|| other_ent.entity_type == &pumpkin_data::entity::EntityType::FALLING_BLOCK
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
impl NBTStorage for TridentEntity {}
|
||||
|
||||
impl EntityBase for TridentEntity {
|
||||
fn tick<'a>(
|
||||
&'a self,
|
||||
caller: &'a Arc<dyn EntityBase>,
|
||||
_server: &'a Server,
|
||||
) -> EntityBaseFuture<'a, ()> {
|
||||
Box::pin(async move {
|
||||
let entity = self.get_entity();
|
||||
let world = entity.world.load();
|
||||
|
||||
// Handle shake time
|
||||
let shake = self.shake_time.load(Ordering::Relaxed);
|
||||
if shake > 0 {
|
||||
self.shake_time.store(shake - 1, Ordering::Relaxed);
|
||||
}
|
||||
|
||||
if self.in_ground.load(Ordering::Relaxed) {
|
||||
let _in_ground_time = self.in_ground_time.fetch_add(1, Ordering::Relaxed);
|
||||
let life = self.life.fetch_add(1, Ordering::Relaxed);
|
||||
|
||||
// Despawn after enough time
|
||||
if life >= Self::DESPAWN_TIME {
|
||||
entity.remove().await;
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// Trident is flying
|
||||
let start_pos = entity.pos.load();
|
||||
let mut velocity = entity.velocity.load();
|
||||
|
||||
// Apply gravity
|
||||
velocity.y -= Self::GRAVITY;
|
||||
|
||||
// Apply inertia (air resistance or water drag)
|
||||
let inertia = if entity.touching_water.load(Ordering::Relaxed) {
|
||||
Self::WATER_INERTIA
|
||||
} else {
|
||||
Self::AIR_INERTIA
|
||||
};
|
||||
velocity = velocity.multiply(inertia, inertia, inertia);
|
||||
|
||||
entity.velocity.store(velocity);
|
||||
|
||||
// Update rotation based on velocity
|
||||
let len = velocity.horizontal_length();
|
||||
entity.set_rotation(
|
||||
velocity.x.atan2(velocity.z) as f32 * 57.295_776,
|
||||
velocity.y.atan2(len) as f32 * 57.295_776,
|
||||
);
|
||||
|
||||
// Move trident
|
||||
let new_pos = start_pos.add(&velocity);
|
||||
entity.set_pos(new_pos);
|
||||
|
||||
// Broadcast velocity update
|
||||
let packet = CEntityVelocity::new(entity.entity_id.into(), velocity);
|
||||
let chunk_pos = entity.chunk_pos.load();
|
||||
world.broadcast_to_chunk(chunk_pos, &packet);
|
||||
|
||||
// Check for collisions using raycasting
|
||||
let search_box = BoundingBox::new(
|
||||
Vector3::new(
|
||||
start_pos.x.min(new_pos.x),
|
||||
start_pos.y.min(new_pos.y),
|
||||
start_pos.z.min(new_pos.z),
|
||||
),
|
||||
Vector3::new(
|
||||
start_pos.x.max(new_pos.x),
|
||||
start_pos.y.max(new_pos.y),
|
||||
start_pos.z.max(new_pos.z),
|
||||
),
|
||||
)
|
||||
.expand(0.3, 0.3, 0.3);
|
||||
|
||||
let mut closest_t = 1.0f64;
|
||||
let mut hit = None;
|
||||
|
||||
// Block collisions
|
||||
let (block_cols, block_positions) = world
|
||||
.get_block_collisions(search_box, self.get_entity())
|
||||
.await;
|
||||
for (idx, bb) in block_cols.iter().enumerate() {
|
||||
if let Some(t) = calculate_ray_intersection(&start_pos, &velocity, bb)
|
||||
&& t < closest_t
|
||||
{
|
||||
closest_t = t;
|
||||
|
||||
// Map back to block pos
|
||||
let mut curr = 0;
|
||||
for (len, pos) in &block_positions {
|
||||
curr += len;
|
||||
if idx < curr {
|
||||
let hit_pos = start_pos.add(&velocity.multiply(t, t, t));
|
||||
hit = Some(ProjectileHit::Block {
|
||||
pos: *pos,
|
||||
face: get_hit_face(hit_pos, *pos),
|
||||
hit_pos,
|
||||
normal: velocity.normalize().multiply(-1.0, -1.0, -1.0),
|
||||
});
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Entity collisions
|
||||
let candidates = world.get_entities_at_box(&search_box);
|
||||
for cand in candidates {
|
||||
if self.should_skip_collision(entity, &cand) {
|
||||
continue;
|
||||
}
|
||||
|
||||
let ebb = cand.get_entity().bounding_box.load().expand(0.3, 0.3, 0.3);
|
||||
if let Some(t) = calculate_ray_intersection(&start_pos, &velocity, &ebb)
|
||||
&& t < closest_t
|
||||
{
|
||||
closest_t = t;
|
||||
let hit_pos = start_pos.add(&velocity.multiply(t, t, t));
|
||||
hit = Some(ProjectileHit::Entity {
|
||||
entity: cand.clone(),
|
||||
hit_pos,
|
||||
normal: velocity.normalize().multiply(-1.0, -1.0, -1.0),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Handle hit
|
||||
if let Some(h) = hit {
|
||||
if !self.has_hit.swap(true, Ordering::SeqCst) {
|
||||
caller.on_hit(h).await;
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
fn get_entity(&self) -> &Entity {
|
||||
&self.entity
|
||||
}
|
||||
|
||||
fn get_living_entity(&self) -> Option<&LivingEntity> {
|
||||
None
|
||||
}
|
||||
|
||||
fn as_nbt_storage(&self) -> &dyn NBTStorage {
|
||||
self
|
||||
}
|
||||
|
||||
fn cast_any(&self) -> &dyn std::any::Any {
|
||||
self
|
||||
}
|
||||
|
||||
fn on_hit(&self, hit: ProjectileHit) -> EntityBaseFuture<'_, ()> {
|
||||
Box::pin(async move {
|
||||
let entity = self.get_entity();
|
||||
let world = entity.world.load();
|
||||
|
||||
match hit {
|
||||
ProjectileHit::Block { pos, hit_pos, .. } => {
|
||||
self.in_ground.store(true, Ordering::Relaxed);
|
||||
self.shake_time.store(7, Ordering::Relaxed);
|
||||
*self.last_block_pos.write().unwrap() = Some(pos);
|
||||
|
||||
// Stop the trident
|
||||
entity.velocity.store(Vector3::new(0.0, 0.0, 0.0));
|
||||
entity.set_pos(hit_pos);
|
||||
|
||||
// Play sound
|
||||
let sound_packet = CSoundEffect::new(
|
||||
IdOr::Id(Sound::ItemTridentHitGround as u16),
|
||||
SoundCategory::Neutral,
|
||||
&hit_pos,
|
||||
1.0,
|
||||
1.0,
|
||||
0.0,
|
||||
);
|
||||
let chunk_pos = entity.chunk_pos.load();
|
||||
world.broadcast_to_chunk(chunk_pos, &sound_packet);
|
||||
}
|
||||
ProjectileHit::Entity {
|
||||
entity: target,
|
||||
hit_pos,
|
||||
..
|
||||
} => {
|
||||
let mut damage = Self::BASE_DAMAGE;
|
||||
|
||||
// Apply Impaling enchantment extra damage
|
||||
if let Some(enchantments) = self
|
||||
.item_stack
|
||||
.lock()
|
||||
.await
|
||||
.get_data_component::<pumpkin_data::data_component_impl::EnchantmentsImpl>(
|
||||
) {
|
||||
for (enchantment, level) in enchantments.enchantment.iter() {
|
||||
if **enchantment == pumpkin_data::Enchantment::IMPALING {
|
||||
let in_water =
|
||||
target.get_entity().touching_water.load(Ordering::Relaxed);
|
||||
if in_water {
|
||||
damage += 1.25 * f64::from(*level);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
target
|
||||
.damage(&*target, damage as f32, DamageType::TRIDENT)
|
||||
.await;
|
||||
|
||||
// Play hit sound
|
||||
let sound_packet = CSoundEffect::new(
|
||||
IdOr::Id(Sound::ItemTridentHit as u16),
|
||||
SoundCategory::Neutral,
|
||||
&hit_pos,
|
||||
1.0,
|
||||
1.0,
|
||||
0.0,
|
||||
);
|
||||
world.broadcast_packet_all(&sound_packet);
|
||||
|
||||
// Standard bounce/fall-back behavior
|
||||
entity.velocity.store(Vector3::new(0.0, -0.1, 0.0));
|
||||
self.has_hit.store(false, Ordering::Relaxed); // Let it hit the ground
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
fn on_player_collision<'a>(&'a self, player: &'a Arc<Player>) -> EntityBaseFuture<'a, ()> {
|
||||
Box::pin(async move {
|
||||
// Can only pick up when on the ground
|
||||
if !self.in_ground.load(Ordering::Relaxed) {
|
||||
return;
|
||||
}
|
||||
|
||||
if player.living_entity.health.load() <= 0.0 {
|
||||
return;
|
||||
}
|
||||
|
||||
match self.pickup {
|
||||
ArrowPickup::Disallowed => return,
|
||||
ArrowPickup::CreativeOnly if !player.is_creative() => return,
|
||||
_ => {}
|
||||
}
|
||||
|
||||
let mut stack = self.item_stack.lock().await.clone();
|
||||
if player.is_creative() || player.inventory.insert_stack_anywhere(&mut stack).await {
|
||||
player.living_entity.pickup(&self.entity, 1);
|
||||
self.get_entity().remove().await;
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
/// Ray intersection algorithm for AABBs, returning a t value
|
||||
fn calculate_ray_intersection(
|
||||
start: &Vector3<f64>,
|
||||
dir: &Vector3<f64>,
|
||||
bb: &BoundingBox,
|
||||
) -> Option<f64> {
|
||||
let mut t_min = 0.0f64;
|
||||
let mut t_max = 1.0f64;
|
||||
|
||||
let b_min = [bb.min.x, bb.min.y, bb.min.z];
|
||||
let b_max = [bb.max.x, bb.max.y, bb.max.z];
|
||||
let s = [start.x, start.y, start.z];
|
||||
let d = [dir.x, dir.y, dir.z];
|
||||
|
||||
for i in 0..3 {
|
||||
if d[i].abs() < 1e-9 {
|
||||
if s[i] < b_min[i] || s[i] > b_max[i] {
|
||||
return None;
|
||||
}
|
||||
} else {
|
||||
let t1 = (b_min[i] - s[i]) / d[i];
|
||||
let t2 = (b_max[i] - s[i]) / d[i];
|
||||
t_min = t_min.max(t1.min(t2));
|
||||
t_max = t_max.min(t1.max(t2));
|
||||
}
|
||||
}
|
||||
|
||||
(0.0..=1.0).contains(&t_min).then_some(t_min)
|
||||
}
|
||||
|
||||
/// Get the face of the block that was hit
|
||||
fn get_hit_face(hit_pos: Vector3<f64>, block_pos: BlockPos) -> pumpkin_data::BlockDirection {
|
||||
let local = hit_pos.sub(&block_pos.0.to_f64());
|
||||
let eps = 1.0e-4;
|
||||
|
||||
if local.x <= eps {
|
||||
pumpkin_data::BlockDirection::West
|
||||
} else if local.x >= 1.0 - eps {
|
||||
pumpkin_data::BlockDirection::East
|
||||
} else if local.y <= eps {
|
||||
pumpkin_data::BlockDirection::Down
|
||||
} else if local.y >= 1.0 - eps {
|
||||
pumpkin_data::BlockDirection::Up
|
||||
} else if local.z <= eps {
|
||||
pumpkin_data::BlockDirection::North
|
||||
} else {
|
||||
pumpkin_data::BlockDirection::South
|
||||
}
|
||||
}
|
||||
@ -105,6 +105,7 @@ 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::tnt::TNTEntity;
|
||||
use crate::entity::vehicle::boat::BoatEntity;
|
||||
@ -238,6 +239,7 @@ pub fn from_type(
|
||||
id if id == EntityType::ITEM.id => Arc::new(ItemEntity::new_for_restore(entity)),
|
||||
id if id == EntityType::ARROW.id => Arc::new(ArrowEntity::new(entity, None)),
|
||||
id if id == EntityType::SPECTRAL_ARROW.id => Arc::new(ArrowEntity::new(entity, None)),
|
||||
id if id == EntityType::TRIDENT.id => Arc::new(TridentEntity::new(entity, None)),
|
||||
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::WIND_CHARGE.id => {
|
||||
|
||||
@ -98,11 +98,27 @@ impl BowItem {
|
||||
|
||||
// Calculate power and fire
|
||||
let power = Self::get_power_for_time(use_ticks);
|
||||
|
||||
// Check for Infinity enchantment
|
||||
let mut has_infinity = false;
|
||||
let held = player.inventory().held_item();
|
||||
if let Some(enchantments) =
|
||||
held.lock()
|
||||
.await
|
||||
.get_data_component::<pumpkin_data::data_component_impl::EnchantmentsImpl>()
|
||||
{
|
||||
has_infinity = enchantments
|
||||
.enchantment
|
||||
.iter()
|
||||
.any(|(e, _)| **e == pumpkin_data::Enchantment::INFINITY);
|
||||
}
|
||||
|
||||
Self.fire_arrow(player, power).await;
|
||||
|
||||
// Consume arrow (if not creative)
|
||||
// Consume arrow (if not creative and no Infinity)
|
||||
if let Some(slot) = arrow_slot
|
||||
&& gamemode != GameMode::Creative
|
||||
&& !has_infinity
|
||||
{
|
||||
player.consume_arrow(slot).await;
|
||||
}
|
||||
@ -147,7 +163,26 @@ impl BowItem {
|
||||
ArrowPickup::Allowed
|
||||
};
|
||||
|
||||
let arrow = ArrowEntity::new_shot(arrow_entity, player.get_entity(), pickup);
|
||||
let mut arrow = ArrowEntity::new_shot(arrow_entity, player.get_entity(), pickup);
|
||||
|
||||
// Read enchantments of the held item (bow)
|
||||
let held = player.inventory().held_item();
|
||||
let stack = held.lock().await;
|
||||
if let Some(enchantments) =
|
||||
stack.get_data_component::<pumpkin_data::data_component_impl::EnchantmentsImpl>()
|
||||
{
|
||||
for (enchantment, level) in enchantments.enchantment.iter() {
|
||||
if **enchantment == pumpkin_data::Enchantment::POWER {
|
||||
arrow.base_damage =
|
||||
arrow.base_damage * (1.0 + 0.25 * (f64::from(*level) + 1.0));
|
||||
} else if **enchantment == pumpkin_data::Enchantment::PUNCH {
|
||||
arrow.punch_level.store(*level as u8, Ordering::Relaxed);
|
||||
} else if **enchantment == pumpkin_data::Enchantment::FLAME {
|
||||
arrow.is_flame.store(true, Ordering::Relaxed);
|
||||
}
|
||||
}
|
||||
}
|
||||
drop(stack);
|
||||
|
||||
// Set velocity based on player's look direction and power
|
||||
let (yaw, pitch) = player.rotation();
|
||||
|
||||
@ -1,10 +1,20 @@
|
||||
use pumpkin_data::item::Item;
|
||||
use pumpkin_util::GameMode;
|
||||
use std::any::Any;
|
||||
use std::future::Future;
|
||||
use std::pin::Pin;
|
||||
use std::sync::Arc;
|
||||
|
||||
use crate::{
|
||||
entity::player::Player,
|
||||
item::{ItemBehaviour, ItemMetadata},
|
||||
};
|
||||
use crate::entity::player::Player;
|
||||
use crate::entity::projectile::arrow::ArrowPickup;
|
||||
use crate::entity::projectile::trident::TridentEntity;
|
||||
use crate::entity::{Entity, EntityBase};
|
||||
use crate::item::{ItemBehaviour, ItemMetadata};
|
||||
use pumpkin_data::entity::EntityType;
|
||||
use pumpkin_data::item::Item;
|
||||
use pumpkin_data::item_stack::ItemStack;
|
||||
use pumpkin_data::sound::{Sound, SoundCategory};
|
||||
use pumpkin_util::GameMode;
|
||||
use pumpkin_util::math::vector3::Vector3;
|
||||
use pumpkin_world::inventory::Inventory;
|
||||
|
||||
pub struct TridentItem;
|
||||
|
||||
@ -15,11 +25,149 @@ impl ItemMetadata for TridentItem {
|
||||
}
|
||||
|
||||
impl ItemBehaviour for TridentItem {
|
||||
fn normal_use<'a>(
|
||||
&'a self,
|
||||
_item: &'a Item,
|
||||
player: &'a Player,
|
||||
) -> Pin<Box<dyn Future<Output = ()> + Send + 'a>> {
|
||||
Box::pin(async move {
|
||||
let inventory = player.inventory();
|
||||
let held = inventory.held_item();
|
||||
let stack = held.lock().await.clone();
|
||||
|
||||
player
|
||||
.living_entity
|
||||
.set_active_hand(pumpkin_util::Hand::Right, stack, 72000)
|
||||
.await;
|
||||
})
|
||||
}
|
||||
|
||||
fn on_stopped_using<'a>(
|
||||
&'a self,
|
||||
_stack: &'a ItemStack,
|
||||
player: &'a Player,
|
||||
) -> Pin<Box<dyn Future<Output = ()> + Send + 'a>> {
|
||||
Box::pin(async move {
|
||||
let use_ticks = player
|
||||
.living_entity
|
||||
.item_use_time
|
||||
.load(std::sync::atomic::Ordering::Relaxed);
|
||||
let use_ticks = 72000 - use_ticks;
|
||||
|
||||
if use_ticks < 10 {
|
||||
return;
|
||||
}
|
||||
|
||||
let world = player.world();
|
||||
let held = player.inventory().held_item();
|
||||
let stack_guard = held.lock().await.clone();
|
||||
|
||||
// Check Riptide level
|
||||
let mut riptide_level = 0u32;
|
||||
if let Some(enchantments) = stack_guard
|
||||
.get_data_component::<pumpkin_data::data_component_impl::EnchantmentsImpl>(
|
||||
) {
|
||||
for (enchantment, level) in enchantments.enchantment.iter() {
|
||||
if **enchantment == pumpkin_data::Enchantment::RIPTIDE {
|
||||
riptide_level = *level as u32;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if riptide_level > 0 {
|
||||
let is_touching_water = player
|
||||
.living_entity
|
||||
.entity
|
||||
.touching_water
|
||||
.load(std::sync::atomic::Ordering::Relaxed);
|
||||
let is_raining = world.is_raining().await;
|
||||
|
||||
if is_touching_water || is_raining {
|
||||
let (yaw, pitch) = player.rotation();
|
||||
let look_vec = Vector3::rotation_vector(pitch as f64, yaw as f64);
|
||||
let speed = f64::from(riptide_level).mul_add(0.75, 1.5);
|
||||
let launch_velocity = look_vec.multiply(speed, speed, speed);
|
||||
|
||||
player.get_entity().set_velocity(launch_velocity);
|
||||
player.get_entity().send_velocity();
|
||||
|
||||
let sound = match riptide_level {
|
||||
1 => Sound::ItemTridentRiptide1,
|
||||
2 => Sound::ItemTridentRiptide2,
|
||||
_ => Sound::ItemTridentRiptide3,
|
||||
};
|
||||
world.play_sound(sound, SoundCategory::Players, &player.position());
|
||||
|
||||
player.living_entity.clear_active_hand().await;
|
||||
|
||||
if player.gamemode.load() != GameMode::Creative {
|
||||
player.damage_held_item(1).await;
|
||||
}
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// Normal throw
|
||||
let entity = Entity::new(world.clone(), player.position(), &EntityType::TRIDENT);
|
||||
let pickup = if player.gamemode.load() == GameMode::Creative {
|
||||
ArrowPickup::CreativeOnly
|
||||
} else {
|
||||
ArrowPickup::Allowed
|
||||
};
|
||||
|
||||
let trident_entity =
|
||||
TridentEntity::new_shot(entity, player.get_entity(), stack_guard, pickup);
|
||||
|
||||
let (yaw, pitch) = player.rotation();
|
||||
trident_entity.set_velocity_from_rotation(pitch, yaw, 0.0, 2.5, 1.0);
|
||||
|
||||
let trident_arc: Arc<dyn EntityBase> = Arc::new(trident_entity);
|
||||
world.spawn_entity(trident_arc).await;
|
||||
|
||||
world.play_sound(
|
||||
Sound::ItemTridentThrow,
|
||||
SoundCategory::Players,
|
||||
&player.position(),
|
||||
);
|
||||
|
||||
if player.gamemode.load() != GameMode::Creative {
|
||||
let inventory = player.inventory();
|
||||
let selected_slot = inventory.get_selected_slot() as usize;
|
||||
|
||||
let main_hand_item = inventory.get_stack(selected_slot).await;
|
||||
let mut stack_lock = main_hand_item.lock().await;
|
||||
if stack_lock.item.id == Item::TRIDENT.id {
|
||||
*stack_lock = ItemStack::EMPTY.clone();
|
||||
player
|
||||
.sync_hand_slot(selected_slot, ItemStack::EMPTY.clone())
|
||||
.await;
|
||||
} else {
|
||||
let off_hand_slot =
|
||||
pumpkin_inventory::player::player_inventory::PlayerInventory::OFF_HAND_SLOT;
|
||||
let off_hand_item = inventory.get_stack(off_hand_slot).await;
|
||||
let mut off_stack_lock = off_hand_item.lock().await;
|
||||
if off_stack_lock.item.id == Item::TRIDENT.id {
|
||||
*off_stack_lock = ItemStack::EMPTY.clone();
|
||||
player
|
||||
.sync_hand_slot(off_hand_slot, ItemStack::EMPTY.clone())
|
||||
.await;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
player.living_entity.clear_active_hand().await;
|
||||
})
|
||||
}
|
||||
|
||||
fn can_mine(&self, player: &Player) -> bool {
|
||||
player.gamemode.load() != GameMode::Creative
|
||||
}
|
||||
|
||||
fn as_any(&self) -> &dyn std::any::Any {
|
||||
fn get_use_duration(&self) -> i32 {
|
||||
72000
|
||||
}
|
||||
|
||||
fn as_any(&self) -> &dyn Any {
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
Loading…
Reference in New Issue
Block a user