fix: anvil
This commit is contained in:
parent
39886a6c55
commit
fdb6da2b0d
134
Cargo.lock
generated
134
Cargo.lock
generated
@ -1786,10 +1786,8 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ff2abc00be7fca6ebc474524697ae276ad847ad0a6b3faa4bcb027e9a4614ad0"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
"js-sys",
|
||||
"libc",
|
||||
"wasi",
|
||||
"wasm-bindgen",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@ -1799,11 +1797,9 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "300e883d756b2e4ec94e02791f39b04b522276138852cfc41d9fb7e904106099"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
"js-sys",
|
||||
"libc",
|
||||
"r-efi",
|
||||
"rand_core 0.10.1",
|
||||
"wasm-bindgen",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@ -2062,22 +2058,6 @@ dependencies = [
|
||||
"want",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "hyper-rustls"
|
||||
version = "0.27.9"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "33ca68d021ef39cf6463ab54c1d0f5daf03377b70561305bb89a8f83aab66e0f"
|
||||
dependencies = [
|
||||
"http",
|
||||
"hyper",
|
||||
"hyper-util",
|
||||
"rustls",
|
||||
"tokio",
|
||||
"tokio-rustls",
|
||||
"tower-service",
|
||||
"webpki-roots",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "hyper-timeout"
|
||||
version = "0.5.2"
|
||||
@ -2466,12 +2446,6 @@ version = "0.4.34"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f9f8bd3e56ce4dfc153cf470fffbfa98c7620958b312ca5c3a4b8d5181fd13c6"
|
||||
|
||||
[[package]]
|
||||
name = "lru-slab"
|
||||
version = "0.1.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "112b39cec0b298b6c1999fee3e31427f74f676e4cb9879ed1a121b43661a4154"
|
||||
|
||||
[[package]]
|
||||
name = "lz4-java-wrc"
|
||||
version = "0.2.0"
|
||||
@ -3591,62 +3565,6 @@ version = "0.1.30"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d55d956fa96f5ec02be2e13af0e20391a5aa83d6a074e3ad368959d0fab299ea"
|
||||
|
||||
[[package]]
|
||||
name = "quinn"
|
||||
version = "0.11.11"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0c1a41e437b6bbd489372cd4971de128e85c855f56c57f283d20ff016cf7c0a8"
|
||||
dependencies = [
|
||||
"bytes",
|
||||
"cfg_aliases",
|
||||
"pin-project-lite",
|
||||
"quinn-proto",
|
||||
"quinn-udp 0.5.15",
|
||||
"rustc-hash",
|
||||
"rustls",
|
||||
"socket2",
|
||||
"thiserror 2.0.20",
|
||||
"tokio",
|
||||
"tracing",
|
||||
"web-time",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "quinn-proto"
|
||||
version = "0.11.17"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "04759210543be93709136e28212294a659ef5001836ff4eab4d663e4529bba83"
|
||||
dependencies = [
|
||||
"bytes",
|
||||
"getrandom 0.4.3",
|
||||
"lru-slab",
|
||||
"rand",
|
||||
"rand_pcg",
|
||||
"ring",
|
||||
"rustc-hash",
|
||||
"rustls",
|
||||
"rustls-pki-types",
|
||||
"slab",
|
||||
"thiserror 2.0.20",
|
||||
"tinyvec",
|
||||
"tracing",
|
||||
"web-time",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "quinn-udp"
|
||||
version = "0.5.15"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "35a133f956daabe89a61a685c2649f13d82d5aa4bd5d12d1277e1072a21c0694"
|
||||
dependencies = [
|
||||
"cfg_aliases",
|
||||
"libc",
|
||||
"once_cell",
|
||||
"socket2",
|
||||
"tracing",
|
||||
"windows-sys 0.61.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "quinn-udp"
|
||||
version = "0.6.1"
|
||||
@ -3710,15 +3628,6 @@ version = "0.10.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "63b8176103e19a2643978565ca18b50549f6101881c443590420e4dc998a3c69"
|
||||
|
||||
[[package]]
|
||||
name = "rand_pcg"
|
||||
version = "0.10.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "caa0f4137e1c0a72f4c651489402276c8e8e1cf081f3b0ba156d2cbeef09e86a"
|
||||
dependencies = [
|
||||
"rand_core 0.10.1",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rayon"
|
||||
version = "1.12.0"
|
||||
@ -3828,35 +3737,28 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "reqwest"
|
||||
version = "0.12.28"
|
||||
version = "0.13.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "eddd3ca559203180a307f12d114c268abf583f59b03cb906fd0b3ff8646c1147"
|
||||
checksum = "219c5811de6525e5416c7d5d53bb656d3afdbc6c5af816e0802bcfa42dbdc1c3"
|
||||
dependencies = [
|
||||
"base64 0.22.1",
|
||||
"bytes",
|
||||
"futures-channel",
|
||||
"futures-core",
|
||||
"futures-util",
|
||||
"h2",
|
||||
"http",
|
||||
"http-body",
|
||||
"http-body-util",
|
||||
"hyper",
|
||||
"hyper-rustls",
|
||||
"hyper-util",
|
||||
"js-sys",
|
||||
"log",
|
||||
"percent-encoding",
|
||||
"pin-project-lite",
|
||||
"quinn",
|
||||
"rustls",
|
||||
"rustls-pki-types",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"serde_urlencoded",
|
||||
"sync_wrapper",
|
||||
"tokio",
|
||||
"tokio-rustls",
|
||||
"tower",
|
||||
"tower-http",
|
||||
"tower-service",
|
||||
@ -3864,7 +3766,6 @@ dependencies = [
|
||||
"wasm-bindgen",
|
||||
"wasm-bindgen-futures",
|
||||
"web-sys",
|
||||
"webpki-roots",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@ -4309,7 +4210,6 @@ version = "1.15.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "2f4925028c7eb5d1fcdaf196971378ed9d2c1c4efc7dc5d011256f76c99c0a96"
|
||||
dependencies = [
|
||||
"web-time",
|
||||
"zeroize",
|
||||
]
|
||||
|
||||
@ -4355,12 +4255,6 @@ version = "0.9.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a252f5e20f038fe7b4ea53e073e65398d652c864cc162fc77c56c2f13717b888"
|
||||
|
||||
[[package]]
|
||||
name = "ryu"
|
||||
version = "1.0.23"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9774ba4a74de5f7b1c1451ed6cd5285a32eddb5cccb8cc655a4e50009e06477f"
|
||||
|
||||
[[package]]
|
||||
name = "same-file"
|
||||
version = "1.0.6"
|
||||
@ -4484,18 +4378,6 @@ dependencies = [
|
||||
"serde_core",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "serde_urlencoded"
|
||||
version = "0.7.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d3491c14715ca2294c4d6a88f15e84739788c1d030eed8c110436aafdaa2f3fd"
|
||||
dependencies = [
|
||||
"form_urlencoded",
|
||||
"itoa",
|
||||
"ryu",
|
||||
"serde",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "serdect"
|
||||
version = "0.4.3"
|
||||
@ -5778,16 +5660,6 @@ dependencies = [
|
||||
"wasm-bindgen",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "web-time"
|
||||
version = "1.1.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5a6580f308b1fad9207618087a65c04e7a10bc77e02c8e84e9b00dd4b12fa0bb"
|
||||
dependencies = [
|
||||
"js-sys",
|
||||
"wasm-bindgen",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "webpki-roots"
|
||||
version = "1.0.9"
|
||||
@ -5810,7 +5682,7 @@ dependencies = [
|
||||
"event-listener",
|
||||
"futures",
|
||||
"log",
|
||||
"quinn-udp 0.6.1",
|
||||
"quinn-udp",
|
||||
"rtc",
|
||||
"tokio",
|
||||
]
|
||||
|
||||
@ -209,7 +209,7 @@ time = { version = "0.3", default-features = false, features = ["std"] }
|
||||
arc-swap = { version = "1.9", default-features = false }
|
||||
tokio-util = { version = "0.7.19", default-features = false }
|
||||
toml = { version = "1.1", default-features = false, features = ["parse", "display", "serde"] }
|
||||
reqwest = { version = "0.12", default-features = false, features = ["rustls-tls", "json", "http2"] }
|
||||
reqwest = { version = "0.13.4", default-features = false, features = ["json"] }
|
||||
notify = { version = "8.2.0", default-features = false, features = ["macos_fsevent"] }
|
||||
|
||||
sysinfo = { version = "0.39", default-features = false, features = ["system"] }
|
||||
|
||||
@ -207,9 +207,77 @@ impl DataComponentImpl for LoreImpl {
|
||||
default_impl!(Lore);
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Hash, PartialEq, Eq)]
|
||||
pub struct RarityImpl;
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, Default)]
|
||||
pub enum Rarity {
|
||||
#[default]
|
||||
Common = 0,
|
||||
Uncommon = 1,
|
||||
Rare = 2,
|
||||
Epic = 3,
|
||||
}
|
||||
|
||||
impl Rarity {
|
||||
#[must_use]
|
||||
pub fn from_id(id: i32) -> Option<Self> {
|
||||
match id {
|
||||
0 => Some(Self::Common),
|
||||
1 => Some(Self::Uncommon),
|
||||
2 => Some(Self::Rare),
|
||||
3 => Some(Self::Epic),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub fn to_id(self) -> i32 {
|
||||
self as i32
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub fn from_name(name: &str) -> Option<Self> {
|
||||
match name {
|
||||
"common" => Some(Self::Common),
|
||||
"uncommon" => Some(Self::Uncommon),
|
||||
"rare" => Some(Self::Rare),
|
||||
"epic" => Some(Self::Epic),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub fn to_name(self) -> &'static str {
|
||||
match self {
|
||||
Self::Common => "common",
|
||||
Self::Uncommon => "uncommon",
|
||||
Self::Rare => "rare",
|
||||
Self::Epic => "epic",
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug, Hash, PartialEq, Eq)]
|
||||
pub struct RarityImpl {
|
||||
pub rarity: Rarity,
|
||||
}
|
||||
|
||||
impl RarityImpl {
|
||||
pub fn read_data(data: &NbtTag) -> Option<Self> {
|
||||
let name = data.extract_string()?;
|
||||
Some(Self {
|
||||
rarity: Rarity::from_name(name)?,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl DataComponentImpl for RarityImpl {
|
||||
fn write_data(&self) -> NbtTag {
|
||||
NbtTag::String(self.rarity.to_name().into())
|
||||
}
|
||||
|
||||
fn get_hash(&self) -> i32 {
|
||||
crate::data_component_impl::get_i32_hash(self.rarity.to_id()) as i32
|
||||
}
|
||||
|
||||
default_impl!(Rarity);
|
||||
}
|
||||
|
||||
|
||||
@ -7,7 +7,10 @@ use crate::data_component_impl::{
|
||||
get_idset_hash, get_str_hash, put_idor,
|
||||
};
|
||||
use crate::entity_type::EntityType;
|
||||
use crate::item::Item;
|
||||
use crate::item_stack::ItemStack;
|
||||
use crate::sound::Sound;
|
||||
use crate::tag::Taggable;
|
||||
use crc_fast::CrcAlgorithm::Crc32Iscsi;
|
||||
use crc_fast::Digest;
|
||||
use pumpkin_nbt::compound::NbtCompound;
|
||||
@ -134,9 +137,23 @@ impl DataComponentImpl for CanBreakImpl {
|
||||
default_impl!(CanBreak);
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Hash, PartialEq, Eq)]
|
||||
pub struct RepairCostImpl;
|
||||
#[derive(Clone, Debug, Hash, PartialEq, Eq, Default)]
|
||||
pub struct RepairCostImpl {
|
||||
pub cost: i32,
|
||||
}
|
||||
impl RepairCostImpl {
|
||||
pub const DEFAULT: Self = Self { cost: 0 };
|
||||
|
||||
pub fn read_data(data: &NbtTag) -> Option<Self> {
|
||||
Some(Self {
|
||||
cost: data.extract_int()?,
|
||||
})
|
||||
}
|
||||
}
|
||||
impl DataComponentImpl for RepairCostImpl {
|
||||
fn write_data(&self) -> NbtTag {
|
||||
NbtTag::Int(self.cost)
|
||||
}
|
||||
default_impl!(RepairCost);
|
||||
}
|
||||
|
||||
@ -608,9 +625,49 @@ impl DataComponentImpl for EquippableImpl {
|
||||
default_impl!(Equippable);
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Hash, PartialEq, Eq)]
|
||||
pub struct RepairableImpl;
|
||||
#[derive(Clone, Debug, PartialEq)]
|
||||
pub struct RepairableImpl {
|
||||
pub items: IDSet<Item>,
|
||||
}
|
||||
|
||||
impl Hash for RepairableImpl {
|
||||
fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
|
||||
get_idset_hash(&self.items).hash(state);
|
||||
}
|
||||
}
|
||||
|
||||
impl RepairableImpl {
|
||||
#[must_use]
|
||||
pub fn is_valid_repair_item(&self, repair_item: &ItemStack) -> bool {
|
||||
if repair_item.is_empty() {
|
||||
return false;
|
||||
}
|
||||
match &self.items {
|
||||
IDSet::Tag(tag) => repair_item.item.is_tagged_with(tag).unwrap_or(false),
|
||||
IDSet::IDs(items) => items.iter().any(|item| item.id == repair_item.item.id),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn read_data(data: &NbtTag) -> Option<Self> {
|
||||
if let NbtTag::Compound(c) = data {
|
||||
let items_tag = c.get("items")?;
|
||||
let items = IDSet::read(items_tag)?;
|
||||
Some(Self { items })
|
||||
} else if let Some(items) = IDSet::read(data) {
|
||||
Some(Self { items })
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl DataComponentImpl for RepairableImpl {
|
||||
fn write_data(&self) -> NbtTag {
|
||||
let mut compound = NbtCompound::new();
|
||||
self.items.write(&mut compound, "items");
|
||||
NbtTag::Compound(compound)
|
||||
}
|
||||
|
||||
default_impl!(Repairable);
|
||||
}
|
||||
|
||||
@ -652,9 +709,102 @@ impl DataComponentImpl for KineticWeaponImpl {
|
||||
default_impl!(KineticWeapon);
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Hash, PartialEq, Eq)]
|
||||
pub struct SwingAnimationImpl;
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, Default)]
|
||||
pub enum SwingAnimationType {
|
||||
#[default]
|
||||
Whack = 0,
|
||||
Stab = 1,
|
||||
None = 2,
|
||||
}
|
||||
|
||||
impl SwingAnimationType {
|
||||
#[must_use]
|
||||
pub fn from_id(id: i32) -> Option<Self> {
|
||||
match id {
|
||||
0 => Some(Self::Whack),
|
||||
1 => Some(Self::Stab),
|
||||
2 => Some(Self::None),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub fn to_id(&self) -> i32 {
|
||||
*self as i32
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub fn to_name(&self) -> &'static str {
|
||||
match self {
|
||||
Self::Whack => "whack",
|
||||
Self::Stab => "stab",
|
||||
Self::None => "none",
|
||||
}
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub fn from_name(name: &str) -> Option<Self> {
|
||||
match name {
|
||||
"whack" => Some(Self::Whack),
|
||||
"stab" => Some(Self::Stab),
|
||||
"none" => Some(Self::None),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug, Hash, PartialEq, Eq)]
|
||||
pub struct SwingAnimationImpl {
|
||||
pub animation_type: SwingAnimationType,
|
||||
pub duration: i32,
|
||||
}
|
||||
|
||||
impl Default for SwingAnimationImpl {
|
||||
fn default() -> Self {
|
||||
Self::DEFAULT
|
||||
}
|
||||
}
|
||||
|
||||
impl SwingAnimationImpl {
|
||||
pub const DEFAULT: Self = Self {
|
||||
animation_type: SwingAnimationType::Whack,
|
||||
duration: 6,
|
||||
};
|
||||
|
||||
pub fn read_data(data: &NbtTag) -> Option<Self> {
|
||||
match data {
|
||||
NbtTag::Compound(compound) => {
|
||||
let animation_type = compound
|
||||
.get("type")
|
||||
.and_then(|tag| match tag {
|
||||
NbtTag::String(name) => SwingAnimationType::from_name(name),
|
||||
NbtTag::Int(id) => SwingAnimationType::from_id(*id),
|
||||
NbtTag::Byte(id) => SwingAnimationType::from_id(*id as i32),
|
||||
_ => None,
|
||||
})
|
||||
.unwrap_or(Self::DEFAULT.animation_type);
|
||||
let duration = compound
|
||||
.get("duration")
|
||||
.and_then(|tag| tag.extract_int())
|
||||
.unwrap_or(Self::DEFAULT.duration);
|
||||
Some(Self {
|
||||
animation_type,
|
||||
duration,
|
||||
})
|
||||
}
|
||||
_ => Some(Self::DEFAULT),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl DataComponentImpl for SwingAnimationImpl {
|
||||
fn write_data(&self) -> NbtTag {
|
||||
let mut compound = NbtCompound::new();
|
||||
compound.put_string("type", self.animation_type.to_name().to_string());
|
||||
compound.put_int("duration", self.duration);
|
||||
NbtTag::Compound(compound)
|
||||
}
|
||||
|
||||
default_impl!(SwingAnimation);
|
||||
}
|
||||
|
||||
|
||||
@ -221,6 +221,25 @@ impl IDSetContent for Block {
|
||||
}
|
||||
}
|
||||
|
||||
impl IDSetContent for crate::item::Item {
|
||||
fn registry_id(&self) -> u16 {
|
||||
self.id
|
||||
}
|
||||
|
||||
fn from_id(id: u16) -> Option<&'static Self> {
|
||||
crate::item::Item::from_id(id)
|
||||
}
|
||||
|
||||
fn from_str(name: &str) -> Option<&'static Self> {
|
||||
let name = name.strip_prefix("minecraft:").unwrap_or(name);
|
||||
crate::item::Item::from_registry_key(name)
|
||||
}
|
||||
|
||||
fn to_string(&self) -> String {
|
||||
self.registry_key.to_string()
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Hash, PartialEq, Debug)]
|
||||
pub enum IDSet<T: IDSetContent + 'static> {
|
||||
Tag(Cow<'static, str>),
|
||||
@ -548,6 +567,8 @@ pub fn read_data(id: DataComponent, data: &NbtTag) -> Option<Box<dyn DataCompone
|
||||
Some(StoredEnchantmentsImpl::read_data(data)?.to_dyn())
|
||||
}
|
||||
DataComponent::UseCooldown => Some(UseCooldownImpl::read_data(data)?.to_dyn()),
|
||||
DataComponent::RepairCost => Some(RepairCostImpl::read_data(data)?.to_dyn()),
|
||||
DataComponent::Repairable => Some(RepairableImpl::read_data(data)?.to_dyn()),
|
||||
DataComponent::MapId => Some(MapIdImpl::read_data(data)?.to_dyn()),
|
||||
DataComponent::ChargedProjectiles => {
|
||||
Some(ChargedProjectilesImpl::read_data(data)?.to_dyn())
|
||||
@ -620,6 +641,8 @@ pub fn read_data(id: DataComponent, data: &NbtTag) -> Option<Box<dyn DataCompone
|
||||
DataComponent::Trim => Some(TrimImpl::read_data(data)?.to_dyn()),
|
||||
DataComponent::CanPlaceOn => Some(CanPlaceOnImpl::read_data(data)?.to_dyn()),
|
||||
DataComponent::CanBreak => Some(CanBreakImpl::read_data(data)?.to_dyn()),
|
||||
DataComponent::SwingAnimation => Some(SwingAnimationImpl::read_data(data)?.to_dyn()),
|
||||
DataComponent::Rarity => Some(RarityImpl::read_data(data)?.to_dyn()),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
@ -845,4 +868,16 @@ mod tests {
|
||||
assert!(enc2.enchantment[0].0 == &crate::Enchantment::SHARPNESS);
|
||||
assert_eq!(enc2.enchantment[0].1, 2);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn swing_animation_round_trip() {
|
||||
assert_round_trip(
|
||||
SwingAnimationImpl {
|
||||
animation_type: SwingAnimationType::Stab,
|
||||
duration: 19,
|
||||
},
|
||||
SwingAnimationImpl::read_data,
|
||||
);
|
||||
assert_round_trip(SwingAnimationImpl::DEFAULT, SwingAnimationImpl::read_data);
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
/* This file is generated. Do not edit manually. */
|
||||
use crate::tag::{RegistryKey, Tag, Taggable};
|
||||
#[derive(Clone, Copy, PartialEq)]
|
||||
#[derive(Clone, Copy, PartialEq, Debug)]
|
||||
pub struct DamageType {
|
||||
pub death_message_type: DeathMessageType,
|
||||
pub exhaustion: f32,
|
||||
@ -9,13 +9,13 @@ pub struct DamageType {
|
||||
pub scaling: DamageScaling,
|
||||
pub id: u8,
|
||||
}
|
||||
#[derive(Clone, Copy, PartialEq)]
|
||||
#[derive(Clone, Copy, PartialEq, Debug)]
|
||||
pub enum DeathMessageType {
|
||||
Default,
|
||||
FallVariants,
|
||||
IntentionalGameDesign,
|
||||
}
|
||||
#[derive(Clone, Copy, PartialEq)]
|
||||
#[derive(Clone, Copy, PartialEq, Debug)]
|
||||
pub enum DamageEffects {
|
||||
Hurt,
|
||||
Thorns,
|
||||
@ -24,7 +24,7 @@ pub enum DamageEffects {
|
||||
Poking,
|
||||
Freezing,
|
||||
}
|
||||
#[derive(Clone, Copy, PartialEq)]
|
||||
#[derive(Clone, Copy, PartialEq, Debug)]
|
||||
pub enum DamageScaling {
|
||||
Never,
|
||||
WhenCausedByLivingNonPlayer,
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@ -1,4 +1,5 @@
|
||||
/* This file is generated. Do not edit manually. */
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
|
||||
pub enum GameEvent {
|
||||
BlockActivate,
|
||||
BlockAttach,
|
||||
@ -62,3 +63,78 @@ pub enum GameEvent {
|
||||
Resonate14,
|
||||
Resonate15,
|
||||
}
|
||||
impl GameEvent {
|
||||
#[must_use]
|
||||
pub fn from_name(name: &str) -> Option<Self> {
|
||||
match name {
|
||||
"block_activate" | "minecraft:block_activate" => Some(Self::BlockActivate),
|
||||
"block_attach" | "minecraft:block_attach" => Some(Self::BlockAttach),
|
||||
"block_change" | "minecraft:block_change" => Some(Self::BlockChange),
|
||||
"block_close" | "minecraft:block_close" => Some(Self::BlockClose),
|
||||
"block_deactivate" | "minecraft:block_deactivate" => Some(Self::BlockDeactivate),
|
||||
"block_destroy" | "minecraft:block_destroy" => Some(Self::BlockDestroy),
|
||||
"block_detach" | "minecraft:block_detach" => Some(Self::BlockDetach),
|
||||
"block_open" | "minecraft:block_open" => Some(Self::BlockOpen),
|
||||
"block_place" | "minecraft:block_place" => Some(Self::BlockPlace),
|
||||
"bounce" | "minecraft:bounce" => Some(Self::Bounce),
|
||||
"container_close" | "minecraft:container_close" => Some(Self::ContainerClose),
|
||||
"container_open" | "minecraft:container_open" => Some(Self::ContainerOpen),
|
||||
"drink" | "minecraft:drink" => Some(Self::Drink),
|
||||
"eat" | "minecraft:eat" => Some(Self::Eat),
|
||||
"elytra_glide" | "minecraft:elytra_glide" => Some(Self::ElytraGlide),
|
||||
"entity_damage" | "minecraft:entity_damage" => Some(Self::EntityDamage),
|
||||
"entity_die" | "minecraft:entity_die" => Some(Self::EntityDie),
|
||||
"entity_dismount" | "minecraft:entity_dismount" => Some(Self::EntityDismount),
|
||||
"entity_interact" | "minecraft:entity_interact" => Some(Self::EntityInteract),
|
||||
"entity_mount" | "minecraft:entity_mount" => Some(Self::EntityMount),
|
||||
"entity_place" | "minecraft:entity_place" => Some(Self::EntityPlace),
|
||||
"entity_action" | "minecraft:entity_action" => Some(Self::EntityAction),
|
||||
"equip" | "minecraft:equip" => Some(Self::Equip),
|
||||
"explode" | "minecraft:explode" => Some(Self::Explode),
|
||||
"flap" | "minecraft:flap" => Some(Self::Flap),
|
||||
"fluid_pickup" | "minecraft:fluid_pickup" => Some(Self::FluidPickup),
|
||||
"fluid_place" | "minecraft:fluid_place" => Some(Self::FluidPlace),
|
||||
"hit_ground" | "minecraft:hit_ground" => Some(Self::HitGround),
|
||||
"instrument_play" | "minecraft:instrument_play" => Some(Self::InstrumentPlay),
|
||||
"item_interact_finish" | "minecraft:item_interact_finish" => {
|
||||
Some(Self::ItemInteractFinish)
|
||||
}
|
||||
"item_interact_start" | "minecraft:item_interact_start" => {
|
||||
Some(Self::ItemInteractStart)
|
||||
}
|
||||
"jukebox_play" | "minecraft:jukebox_play" => Some(Self::JukeboxPlay),
|
||||
"jukebox_stop_play" | "minecraft:jukebox_stop_play" => Some(Self::JukeboxStopPlay),
|
||||
"lightning_strike" | "minecraft:lightning_strike" => Some(Self::LightningStrike),
|
||||
"note_block_play" | "minecraft:note_block_play" => Some(Self::NoteBlockPlay),
|
||||
"prime_fuse" | "minecraft:prime_fuse" => Some(Self::PrimeFuse),
|
||||
"projectile_land" | "minecraft:projectile_land" => Some(Self::ProjectileLand),
|
||||
"projectile_shoot" | "minecraft:projectile_shoot" => Some(Self::ProjectileShoot),
|
||||
"sculk_sensor_tendrils_clicking" | "minecraft:sculk_sensor_tendrils_clicking" => {
|
||||
Some(Self::SculkSensorTendrilsClicking)
|
||||
}
|
||||
"shear" | "minecraft:shear" => Some(Self::Shear),
|
||||
"shriek" | "minecraft:shriek" => Some(Self::Shriek),
|
||||
"splash" | "minecraft:splash" => Some(Self::Splash),
|
||||
"step" | "minecraft:step" => Some(Self::Step),
|
||||
"swim" | "minecraft:swim" => Some(Self::Swim),
|
||||
"teleport" | "minecraft:teleport" => Some(Self::Teleport),
|
||||
"unequip" | "minecraft:unequip" => Some(Self::Unequip),
|
||||
"resonate_1" | "minecraft:resonate_1" => Some(Self::Resonate1),
|
||||
"resonate_2" | "minecraft:resonate_2" => Some(Self::Resonate2),
|
||||
"resonate_3" | "minecraft:resonate_3" => Some(Self::Resonate3),
|
||||
"resonate_4" | "minecraft:resonate_4" => Some(Self::Resonate4),
|
||||
"resonate_5" | "minecraft:resonate_5" => Some(Self::Resonate5),
|
||||
"resonate_6" | "minecraft:resonate_6" => Some(Self::Resonate6),
|
||||
"resonate_7" | "minecraft:resonate_7" => Some(Self::Resonate7),
|
||||
"resonate_8" | "minecraft:resonate_8" => Some(Self::Resonate8),
|
||||
"resonate_9" | "minecraft:resonate_9" => Some(Self::Resonate9),
|
||||
"resonate_10" | "minecraft:resonate_10" => Some(Self::Resonate10),
|
||||
"resonate_11" | "minecraft:resonate_11" => Some(Self::Resonate11),
|
||||
"resonate_12" | "minecraft:resonate_12" => Some(Self::Resonate12),
|
||||
"resonate_13" | "minecraft:resonate_13" => Some(Self::Resonate13),
|
||||
"resonate_14" | "minecraft:resonate_14" => Some(Self::Resonate14),
|
||||
"resonate_15" | "minecraft:resonate_15" => Some(Self::Resonate15),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because one or more lines are too long
@ -17777,8 +17777,19 @@ pub trait Taggable {
|
||||
fn registry_id(&self) -> u16;
|
||||
#[must_use]
|
||||
fn is_tagged_with(&self, tag: &str) -> Option<bool> {
|
||||
let tag = tag.strip_prefix("#").unwrap_or(tag);
|
||||
let items = get_tag_ids(Self::tag_key(), tag)?;
|
||||
let tag = tag.strip_prefix('#').unwrap_or(tag);
|
||||
let items = get_tag_ids(Self::tag_key(), tag).or_else(|| {
|
||||
if !tag.contains(':') {
|
||||
let mut full = String::with_capacity(10 + tag.len());
|
||||
full.push_str("minecraft:");
|
||||
full.push_str(tag);
|
||||
get_tag_ids(Self::tag_key(), &full)
|
||||
} else if let Some(stripped) = tag.strip_prefix("minecraft:") {
|
||||
get_tag_ids(Self::tag_key(), stripped)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
})?;
|
||||
Some(items.contains(&self.registry_id()))
|
||||
}
|
||||
#[must_use]
|
||||
@ -17787,7 +17798,18 @@ pub trait Taggable {
|
||||
}
|
||||
#[must_use]
|
||||
fn get_tag_values(tag: &str) -> Option<&'static [&'static str]> {
|
||||
let tag = tag.strip_prefix("#").unwrap_or(tag);
|
||||
get_tag_values(Self::tag_key(), tag)
|
||||
let tag = tag.strip_prefix('#').unwrap_or(tag);
|
||||
get_tag_values(Self::tag_key(), tag).or_else(|| {
|
||||
if !tag.contains(':') {
|
||||
let mut full = String::with_capacity(10 + tag.len());
|
||||
full.push_str("minecraft:");
|
||||
full.push_str(tag);
|
||||
get_tag_values(Self::tag_key(), &full)
|
||||
} else if let Some(stripped) = tag.strip_prefix("minecraft:") {
|
||||
get_tag_values(Self::tag_key(), stripped)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@ -2,9 +2,10 @@ use crate::data_component::DataComponent;
|
||||
use crate::data_component::DataComponent::Enchantments;
|
||||
use crate::data_component_impl::{
|
||||
BlocksAttacksImpl, ConsumableImpl, CustomDataImpl, DamageImpl, DataComponentImpl,
|
||||
EnchantmentsImpl, IDSet, MaxDamageImpl, MaxStackSizeImpl, ToolImpl, UnbreakableImpl,
|
||||
UseCooldownImpl, get, get_mut, read_data,
|
||||
EnchantmentsImpl, IDSet, MaxDamageImpl, MaxStackSizeImpl, Rarity, RarityImpl,
|
||||
SwingAnimationImpl, ToolImpl, UnbreakableImpl, UseCooldownImpl, get, get_mut, read_data,
|
||||
};
|
||||
|
||||
use crate::item::Item;
|
||||
use crate::recipes::RecipeResultStruct;
|
||||
use crate::tag::Taggable;
|
||||
@ -187,6 +188,21 @@ impl ItemStack {
|
||||
None
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub fn has_data_component(&self, to_get_id: DataComponent) -> bool {
|
||||
for (id, component) in &self.patch {
|
||||
if *id == to_get_id {
|
||||
return component.is_some();
|
||||
}
|
||||
}
|
||||
for (id, _) in self.item.components {
|
||||
if *id == to_get_id {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
false
|
||||
}
|
||||
|
||||
pub fn has_enchantments(&self) -> bool {
|
||||
self.get_data_component::<EnchantmentsImpl>()
|
||||
.is_some_and(|e| !e.enchantment.is_empty())
|
||||
@ -446,6 +462,81 @@ impl ItemStack {
|
||||
}
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub fn has_custom_name(&self) -> bool {
|
||||
self.get_data_component::<crate::data_component_impl::CustomNameImpl>()
|
||||
.is_some()
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub fn get_custom_name(&self) -> Option<&pumpkin_util::text::TextComponent> {
|
||||
self.get_data_component::<crate::data_component_impl::CustomNameImpl>()
|
||||
.map(|c| &c.name)
|
||||
}
|
||||
|
||||
pub fn remove_custom_name(&mut self) {
|
||||
self.patch
|
||||
.retain(|(id, _)| *id != DataComponent::CustomName);
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub fn get_hover_name(&self) -> String {
|
||||
if let Some(custom_name) =
|
||||
self.get_data_component::<crate::data_component_impl::CustomNameImpl>()
|
||||
{
|
||||
return custom_name.name.clone().get_text();
|
||||
}
|
||||
if let Some(item_name) =
|
||||
self.get_data_component::<crate::data_component_impl::ItemNameImpl>()
|
||||
{
|
||||
return item_name.name.to_string();
|
||||
}
|
||||
self.item.registry_key.to_string()
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub fn get_repair_cost(&self) -> i32 {
|
||||
self.get_data_component::<crate::data_component_impl::RepairCostImpl>()
|
||||
.map_or(0, |value| value.cost)
|
||||
}
|
||||
|
||||
pub fn set_repair_cost(&mut self, cost: i32) {
|
||||
if cost <= 0 {
|
||||
self.patch
|
||||
.retain(|(id, _)| *id != DataComponent::RepairCost);
|
||||
return;
|
||||
}
|
||||
self.set_data_component(crate::data_component_impl::RepairCostImpl { cost });
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub fn is_valid_repair_item(&self, repair_item: &ItemStack) -> bool {
|
||||
let repairable = self.get_data_component::<crate::data_component_impl::RepairableImpl>();
|
||||
repairable.is_some_and(|r| r.is_valid_repair_item(repair_item))
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub fn get_swing_animation(&self) -> SwingAnimationImpl {
|
||||
self.get_data_component::<SwingAnimationImpl>()
|
||||
.copied()
|
||||
.unwrap_or(SwingAnimationImpl::DEFAULT)
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub fn get_rarity(&self) -> Rarity {
|
||||
let base = self
|
||||
.get_data_component::<RarityImpl>()
|
||||
.map_or(Rarity::Common, |r| r.rarity);
|
||||
if !self.has_enchantments() {
|
||||
return base;
|
||||
}
|
||||
match base {
|
||||
Rarity::Common | Rarity::Uncommon => Rarity::Rare,
|
||||
Rarity::Rare => Rarity::Epic,
|
||||
Rarity::Epic => Rarity::Epic,
|
||||
}
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub fn custom_data_compound(&self) -> Option<&NbtCompound> {
|
||||
self.get_data_component::<CustomDataImpl>()
|
||||
|
||||
@ -1,21 +1,184 @@
|
||||
use std::any::Any;
|
||||
use std::borrow::Cow;
|
||||
use std::sync::Arc;
|
||||
use std::sync::atomic::{AtomicBool, AtomicI32, AtomicU8, Ordering};
|
||||
|
||||
use pumpkin_data::{item_stack::ItemStack, screen::WindowType};
|
||||
use pumpkin_data::Enchantment;
|
||||
use pumpkin_data::data_component::DataComponent;
|
||||
use pumpkin_data::data_component_impl::{EnchantmentsImpl, StoredEnchantmentsImpl};
|
||||
use pumpkin_data::item::Item;
|
||||
use pumpkin_data::item_stack::ItemStack;
|
||||
use pumpkin_data::screen::WindowType;
|
||||
use pumpkin_world::inventory::Inventory;
|
||||
|
||||
use crate::{
|
||||
player::player_inventory::PlayerInventory,
|
||||
screen_handler::{InventoryPlayer, ScreenHandler, ScreenHandlerBehaviour, offer_or_drop_stack},
|
||||
slot::NormalSlot,
|
||||
slot::{NormalSlot, Slot},
|
||||
window_property::{Anvil, WindowProperty},
|
||||
};
|
||||
|
||||
pub struct AnvilResultSlot {
|
||||
pub inventory: Arc<dyn Inventory>,
|
||||
pub index: usize,
|
||||
pub id: AtomicU8,
|
||||
pub cost: Arc<AtomicI32>,
|
||||
pub repair_item_count_cost: Arc<AtomicI32>,
|
||||
pub only_renaming: Arc<AtomicBool>,
|
||||
}
|
||||
|
||||
impl AnvilResultSlot {
|
||||
#[must_use]
|
||||
pub fn new(
|
||||
inventory: Arc<dyn Inventory>,
|
||||
index: usize,
|
||||
cost: Arc<AtomicI32>,
|
||||
repair_item_count_cost: Arc<AtomicI32>,
|
||||
only_renaming: Arc<AtomicBool>,
|
||||
) -> Self {
|
||||
Self {
|
||||
inventory,
|
||||
index,
|
||||
id: AtomicU8::new(0),
|
||||
cost,
|
||||
repair_item_count_cost,
|
||||
only_renaming,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Slot for AnvilResultSlot {
|
||||
fn get_inventory(&self) -> Arc<dyn Inventory> {
|
||||
self.inventory.clone()
|
||||
}
|
||||
|
||||
fn get_index(&self) -> usize {
|
||||
self.index
|
||||
}
|
||||
|
||||
fn set_id(&self, id: usize) {
|
||||
self.id.store(id as u8, Ordering::Relaxed);
|
||||
}
|
||||
|
||||
fn can_insert(&self, _stack: &ItemStack) -> bool {
|
||||
false
|
||||
}
|
||||
|
||||
fn can_take_items(&self, player: &dyn InventoryPlayer) -> bool {
|
||||
let cost = self.cost.load(Ordering::Relaxed);
|
||||
(player.has_infinite_materials() || player.experience_level() >= cost) && cost > 0
|
||||
}
|
||||
|
||||
fn on_take_item(&self, player: &dyn InventoryPlayer, _stack: &ItemStack) {
|
||||
let cost = self.cost.load(Ordering::Relaxed);
|
||||
if !player.has_infinite_materials() {
|
||||
player.add_experience_levels(-cost);
|
||||
}
|
||||
|
||||
let repair_item_count_cost = self.repair_item_count_cost.load(Ordering::Relaxed);
|
||||
if repair_item_count_cost > 0 {
|
||||
let mut addition = self.inventory.get_stack(1);
|
||||
if !addition.is_empty() && addition.item_count > repair_item_count_cost as u8 {
|
||||
addition.decrement(repair_item_count_cost as u8);
|
||||
self.inventory.set_stack(1, addition);
|
||||
} else {
|
||||
self.inventory.set_stack(1, ItemStack::EMPTY.clone());
|
||||
}
|
||||
} else if !self.only_renaming.load(Ordering::Relaxed) {
|
||||
self.inventory.set_stack(1, ItemStack::EMPTY.clone());
|
||||
}
|
||||
|
||||
self.cost.store(0, Ordering::Relaxed);
|
||||
self.inventory.set_stack(0, ItemStack::EMPTY.clone());
|
||||
player.use_anvil();
|
||||
self.mark_dirty();
|
||||
}
|
||||
|
||||
fn mark_dirty(&self) {
|
||||
self.inventory.mark_dirty();
|
||||
}
|
||||
}
|
||||
|
||||
pub struct AnvilScreenHandler {
|
||||
pub inventory: Arc<dyn Inventory>,
|
||||
behaviour: ScreenHandlerBehaviour,
|
||||
pub rename_text: String,
|
||||
pub repair_cost: i16,
|
||||
pub item_name: Option<String>,
|
||||
pub repair_cost: Arc<AtomicI32>,
|
||||
pub repair_item_count_cost: Arc<AtomicI32>,
|
||||
pub only_renaming: Arc<AtomicBool>,
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub fn validate_name(name: &str) -> Option<String> {
|
||||
let filtered: String = name.chars().filter(|&c| c >= ' ' && c != '\x7F').collect();
|
||||
(filtered.chars().count() <= 50).then_some(filtered)
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub const fn calculate_increased_repair_cost(base_cost: i32) -> i32 {
|
||||
let doubled = (base_cost as i64) * 2 + 1;
|
||||
if doubled > i32::MAX as i64 {
|
||||
i32::MAX
|
||||
} else {
|
||||
doubled as i32
|
||||
}
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub fn get_component_type(stack: &ItemStack) -> DataComponent {
|
||||
if stack.item == &Item::ENCHANTED_BOOK {
|
||||
DataComponent::StoredEnchantments
|
||||
} else {
|
||||
DataComponent::Enchantments
|
||||
}
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub fn can_store_enchantments(stack: &ItemStack) -> bool {
|
||||
if stack.is_empty() {
|
||||
return false;
|
||||
}
|
||||
stack.has_data_component(get_component_type(stack))
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub fn get_enchantments_for_crafting(stack: &ItemStack) -> Vec<(&'static Enchantment, i32)> {
|
||||
if stack.is_empty() {
|
||||
return Vec::new();
|
||||
}
|
||||
if stack.item == &Item::ENCHANTED_BOOK {
|
||||
if let Some(stored) = stack.get_data_component::<StoredEnchantmentsImpl>() {
|
||||
return stored
|
||||
.enchantment
|
||||
.iter()
|
||||
.map(|(e, lvl)| (*e, *lvl))
|
||||
.collect();
|
||||
}
|
||||
} else if let Some(ench) = stack.get_data_component::<EnchantmentsImpl>() {
|
||||
return ench.enchantment.iter().map(|(e, lvl)| (*e, *lvl)).collect();
|
||||
}
|
||||
Vec::new()
|
||||
}
|
||||
|
||||
pub fn set_enchantments(result: &mut ItemStack, enchantments: Vec<(&'static Enchantment, i32)>) {
|
||||
if result.is_empty() {
|
||||
return;
|
||||
}
|
||||
if result.item == &Item::ENCHANTED_BOOK
|
||||
|| result
|
||||
.get_data_component::<StoredEnchantmentsImpl>()
|
||||
.is_some()
|
||||
{
|
||||
let stored = StoredEnchantmentsImpl {
|
||||
enchantment: Cow::Owned(enchantments),
|
||||
};
|
||||
result.set_data_component(stored);
|
||||
} else {
|
||||
let ench = EnchantmentsImpl {
|
||||
enchantment: Cow::Owned(enchantments),
|
||||
};
|
||||
result.set_data_component(ench);
|
||||
}
|
||||
}
|
||||
|
||||
impl AnvilScreenHandler {
|
||||
@ -25,17 +188,31 @@ impl AnvilScreenHandler {
|
||||
player_inventory: &Arc<PlayerInventory>,
|
||||
inventory: Arc<dyn Inventory>,
|
||||
) -> Self {
|
||||
let cost = Arc::new(AtomicI32::new(0));
|
||||
let repair_item_count_cost = Arc::new(AtomicI32::new(0));
|
||||
let only_renaming = Arc::new(AtomicBool::new(false));
|
||||
|
||||
let mut handler = Self {
|
||||
inventory: inventory.clone(),
|
||||
behaviour: ScreenHandlerBehaviour::new(sync_id, Some(WindowType::Anvil)),
|
||||
rename_text: String::new(),
|
||||
repair_cost: 0,
|
||||
item_name: None,
|
||||
repair_cost: cost.clone(),
|
||||
repair_item_count_cost: repair_item_count_cost.clone(),
|
||||
only_renaming: only_renaming.clone(),
|
||||
};
|
||||
|
||||
// Anvil specific slots: 2 input, 1 output
|
||||
for i in 0..3 {
|
||||
handler.add_slot(Arc::new(NormalSlot::new(inventory.clone(), i)));
|
||||
}
|
||||
// Slot 0: Input slot
|
||||
handler.add_slot(Arc::new(NormalSlot::new(inventory.clone(), 0)));
|
||||
// Slot 1: Additional slot
|
||||
handler.add_slot(Arc::new(NormalSlot::new(inventory.clone(), 1)));
|
||||
// Slot 2: Result slot
|
||||
handler.add_slot(Arc::new(AnvilResultSlot::new(
|
||||
inventory.clone(),
|
||||
2,
|
||||
cost,
|
||||
repair_item_count_cost,
|
||||
only_renaming,
|
||||
)));
|
||||
|
||||
let player_inventory: Arc<dyn Inventory> = player_inventory.clone();
|
||||
handler.add_player_slots(&player_inventory);
|
||||
@ -43,46 +220,42 @@ impl AnvilScreenHandler {
|
||||
handler
|
||||
}
|
||||
|
||||
pub fn update_item_name(&mut self, name: String) {
|
||||
self.rename_text = name;
|
||||
self.update_result_slot();
|
||||
self.send_content_updates();
|
||||
pub fn set_item_name(&mut self, name: &str, has_infinite_materials: bool) -> bool {
|
||||
let validated_name = validate_name(name);
|
||||
if let Some(validated_name) = validated_name
|
||||
&& self.item_name.as_deref() != Some(&validated_name)
|
||||
{
|
||||
self.item_name = Some(validated_name.clone());
|
||||
let result_stack = self.inventory.get_stack(2);
|
||||
if !result_stack.is_empty() {
|
||||
let mut updated_stack = result_stack;
|
||||
if validated_name.trim().is_empty() {
|
||||
updated_stack.remove_custom_name();
|
||||
} else {
|
||||
updated_stack.set_custom_name(validated_name);
|
||||
}
|
||||
self.inventory.set_stack(2, updated_stack);
|
||||
}
|
||||
self.create_result(has_infinite_materials);
|
||||
return true;
|
||||
}
|
||||
false
|
||||
}
|
||||
|
||||
pub fn update_item_name(&mut self, name: &str) {
|
||||
self.set_item_name(name, false);
|
||||
}
|
||||
|
||||
pub fn update_result_slot(&mut self) {
|
||||
let input_a = self.inventory.get_stack(0);
|
||||
|
||||
if input_a.is_empty() {
|
||||
self.inventory.set_stack(2, ItemStack::EMPTY.clone());
|
||||
self.set_repair_cost(0);
|
||||
return;
|
||||
}
|
||||
|
||||
let mut result_item = input_a;
|
||||
let mut cost = 0;
|
||||
|
||||
// Basic renaming logic for now
|
||||
if !self.rename_text.is_empty() {
|
||||
result_item.set_custom_name(self.rename_text.clone());
|
||||
cost += 1;
|
||||
}
|
||||
|
||||
// If combining with another item... we'll skip complex anvil logic for now
|
||||
// and just support renaming.
|
||||
if cost > 0 {
|
||||
self.inventory.set_stack(2, result_item);
|
||||
self.set_repair_cost(cost);
|
||||
} else {
|
||||
self.inventory.set_stack(2, ItemStack::EMPTY.clone());
|
||||
self.set_repair_cost(0);
|
||||
}
|
||||
self.create_result(false);
|
||||
}
|
||||
|
||||
pub fn set_repair_cost(&mut self, cost: i16) {
|
||||
self.repair_cost = cost;
|
||||
pub fn set_repair_cost(&mut self, cost: i32) {
|
||||
self.repair_cost.store(cost, Ordering::Relaxed);
|
||||
if let Some(sync_handler) = self.behaviour.sync_handler.as_ref() {
|
||||
let (property_id, property_value) =
|
||||
WindowProperty::new(Anvil::RepairCost, cost).into_tuple();
|
||||
WindowProperty::new(Anvil::RepairCost, cost.clamp(0, i16::MAX as i32) as i16)
|
||||
.into_tuple();
|
||||
sync_handler.update_property(
|
||||
&self.behaviour,
|
||||
property_id as i32,
|
||||
@ -90,6 +263,200 @@ impl AnvilScreenHandler {
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub fn get_cost(&self) -> i32 {
|
||||
self.repair_cost.load(Ordering::Relaxed)
|
||||
}
|
||||
|
||||
#[allow(clippy::too_many_lines)]
|
||||
pub fn create_result(&mut self, has_infinite_materials: bool) {
|
||||
let input = self.inventory.get_stack(0);
|
||||
self.only_renaming.store(false, Ordering::Relaxed);
|
||||
self.set_repair_cost(1);
|
||||
let mut price: i32 = 0;
|
||||
let mut tax: i64 = 0;
|
||||
let mut naming_cost: i32 = 0;
|
||||
|
||||
if !input.is_empty() && can_store_enchantments(&input) {
|
||||
let mut result = input.clone();
|
||||
let addition = self.inventory.get_stack(1);
|
||||
let mut enchantments = get_enchantments_for_crafting(&result);
|
||||
tax += input.get_repair_cost() as i64 + addition.get_repair_cost() as i64;
|
||||
self.repair_item_count_cost.store(0, Ordering::Relaxed);
|
||||
|
||||
if !addition.is_empty() {
|
||||
let using_book = addition.item == &Item::ENCHANTED_BOOK
|
||||
|| addition
|
||||
.get_data_component::<StoredEnchantmentsImpl>()
|
||||
.is_some_and(|s| !s.enchantment.is_empty());
|
||||
|
||||
if result.is_damageable() && input.is_valid_repair_item(&addition) {
|
||||
let max_damage = result.get_max_damage().unwrap_or(0);
|
||||
let mut repair_amount = result.get_damage().min(max_damage / 4);
|
||||
if repair_amount <= 0 {
|
||||
self.inventory.set_stack(2, ItemStack::EMPTY.clone());
|
||||
self.set_repair_cost(0);
|
||||
return;
|
||||
}
|
||||
|
||||
let mut count = 0;
|
||||
while repair_amount > 0 && count < addition.item_count as i32 {
|
||||
let result_damage = result.get_damage() - repair_amount;
|
||||
result.set_damage(result_damage);
|
||||
price += 1;
|
||||
repair_amount = result.get_damage().min(max_damage / 4);
|
||||
count += 1;
|
||||
}
|
||||
|
||||
self.repair_item_count_cost.store(count, Ordering::Relaxed);
|
||||
} else {
|
||||
if !using_book && (result.item != addition.item || !result.is_damageable()) {
|
||||
self.inventory.set_stack(2, ItemStack::EMPTY.clone());
|
||||
self.set_repair_cost(0);
|
||||
return;
|
||||
}
|
||||
|
||||
if result.is_damageable() && !using_book {
|
||||
let max_damage = result.get_max_damage().unwrap_or(0);
|
||||
let remaining1 = max_damage - input.get_damage();
|
||||
let remaining2 =
|
||||
addition.get_max_damage().unwrap_or(0) - addition.get_damage();
|
||||
let additional = remaining2 + max_damage * 12 / 100;
|
||||
let remaining = remaining1 + additional;
|
||||
let mut result_damage = max_damage - remaining;
|
||||
if result_damage < 0 {
|
||||
result_damage = 0;
|
||||
}
|
||||
|
||||
if result_damage < result.get_damage() {
|
||||
result.set_damage(result_damage);
|
||||
price += 2;
|
||||
}
|
||||
}
|
||||
|
||||
let additional_enchantments = get_enchantments_for_crafting(&addition);
|
||||
let mut is_any_enchantment_compatible = false;
|
||||
let mut is_any_enchantment_not_compatible = false;
|
||||
|
||||
for (enchantment, entry_level) in additional_enchantments {
|
||||
let current = enchantments
|
||||
.iter()
|
||||
.find(|(e, _)| *e == enchantment)
|
||||
.map_or(0, |(_, lvl)| *lvl);
|
||||
let mut level = if current == entry_level {
|
||||
entry_level + 1
|
||||
} else {
|
||||
entry_level.max(current)
|
||||
};
|
||||
|
||||
let mut compatible =
|
||||
if has_infinite_materials || input.item == &Item::ENCHANTED_BOOK {
|
||||
true
|
||||
} else {
|
||||
enchantment.can_enchant(input.item)
|
||||
};
|
||||
|
||||
for (other, _) in &enchantments {
|
||||
if *other != enchantment && !other.are_compatible(enchantment) {
|
||||
compatible = false;
|
||||
price += 1;
|
||||
}
|
||||
}
|
||||
|
||||
if compatible {
|
||||
is_any_enchantment_compatible = true;
|
||||
if level > enchantment.max_level {
|
||||
level = enchantment.max_level;
|
||||
}
|
||||
|
||||
if let Some(entry) =
|
||||
enchantments.iter_mut().find(|(e, _)| *e == enchantment)
|
||||
{
|
||||
entry.1 = level;
|
||||
} else {
|
||||
enchantments.push((enchantment, level));
|
||||
}
|
||||
|
||||
let mut fee = enchantment.anvil_cost as i32;
|
||||
if using_book {
|
||||
fee = 1.max(fee / 2);
|
||||
}
|
||||
|
||||
price += fee * level;
|
||||
if input.item_count > 1 {
|
||||
price = 40;
|
||||
}
|
||||
} else {
|
||||
is_any_enchantment_not_compatible = true;
|
||||
}
|
||||
}
|
||||
|
||||
if is_any_enchantment_not_compatible && !is_any_enchantment_compatible {
|
||||
self.inventory.set_stack(2, ItemStack::EMPTY.clone());
|
||||
self.set_repair_cost(0);
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let input_hover_name = input.get_hover_name();
|
||||
if let Some(item_name) = &self.item_name
|
||||
&& !item_name.trim().is_empty()
|
||||
{
|
||||
if item_name != &input_hover_name {
|
||||
naming_cost = 1;
|
||||
price += naming_cost;
|
||||
result.set_custom_name(item_name.clone());
|
||||
}
|
||||
} else if input.has_custom_name() {
|
||||
naming_cost = 1;
|
||||
price += naming_cost;
|
||||
result.remove_custom_name();
|
||||
}
|
||||
|
||||
let final_price = if price <= 0 {
|
||||
0
|
||||
} else {
|
||||
(tax + price as i64).clamp(0, i32::MAX as i64) as i32
|
||||
};
|
||||
self.set_repair_cost(final_price);
|
||||
if price <= 0 {
|
||||
result = ItemStack::EMPTY.clone();
|
||||
}
|
||||
|
||||
if naming_cost == price && naming_cost > 0 {
|
||||
if self.repair_cost.load(Ordering::Relaxed) >= 40 {
|
||||
self.set_repair_cost(39);
|
||||
}
|
||||
self.only_renaming.store(true, Ordering::Relaxed);
|
||||
}
|
||||
|
||||
if self.repair_cost.load(Ordering::Relaxed) >= 40 && !has_infinite_materials {
|
||||
result = ItemStack::EMPTY.clone();
|
||||
}
|
||||
|
||||
if !result.is_empty() {
|
||||
let mut base_cost = result.get_repair_cost();
|
||||
if base_cost < addition.get_repair_cost() {
|
||||
base_cost = addition.get_repair_cost();
|
||||
}
|
||||
|
||||
if naming_cost != price || naming_cost == 0 {
|
||||
base_cost = calculate_increased_repair_cost(base_cost);
|
||||
}
|
||||
|
||||
result.set_repair_cost(base_cost);
|
||||
set_enchantments(&mut result, enchantments);
|
||||
}
|
||||
|
||||
self.inventory.set_stack(2, result);
|
||||
self.send_content_updates();
|
||||
} else {
|
||||
self.inventory.set_stack(2, ItemStack::EMPTY.clone());
|
||||
self.set_repair_cost(0);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl ScreenHandler for AnvilScreenHandler {
|
||||
@ -130,16 +497,32 @@ impl ScreenHandler for AnvilScreenHandler {
|
||||
let mut slot_stack = slot.get_stack();
|
||||
stack_left = slot_stack.clone();
|
||||
|
||||
if slot_index < 3 {
|
||||
// From anvil to player
|
||||
if !self.insert_item(&mut slot_stack, 3, 39, true) {
|
||||
return ItemStack::EMPTY.clone();
|
||||
match slot_index.cmp(&2) {
|
||||
std::cmp::Ordering::Equal => {
|
||||
// Result slot: from anvil to player
|
||||
if !slot.can_take_items(player) {
|
||||
return ItemStack::EMPTY.clone();
|
||||
}
|
||||
if !self.insert_item(&mut slot_stack, 3, 39, true) {
|
||||
return ItemStack::EMPTY.clone();
|
||||
}
|
||||
slot.on_quick_move_crafted(slot_stack.clone(), stack_left.clone());
|
||||
slot.on_take_item(player, &stack_left);
|
||||
self.create_result(player.has_infinite_materials());
|
||||
self.send_content_updates();
|
||||
return stack_left;
|
||||
}
|
||||
slot.on_quick_move_crafted(slot_stack.clone(), stack_left.clone());
|
||||
} else {
|
||||
// From player to anvil
|
||||
if !self.insert_item(&mut slot_stack, 0, 2, false) {
|
||||
return ItemStack::EMPTY.clone();
|
||||
std::cmp::Ordering::Less => {
|
||||
// Input slots (0, 1): from anvil to player
|
||||
if !self.insert_item(&mut slot_stack, 3, 39, false) {
|
||||
return ItemStack::EMPTY.clone();
|
||||
}
|
||||
}
|
||||
std::cmp::Ordering::Greater => {
|
||||
// From player to anvil input slots (0..2)
|
||||
if !self.insert_item(&mut slot_stack, 0, 2, false) {
|
||||
return ItemStack::EMPTY.clone();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -150,6 +533,8 @@ impl ScreenHandler for AnvilScreenHandler {
|
||||
slot.set_stack_prev(slot_stack.clone(), stack_left.clone());
|
||||
slot.on_take_item(player, &slot_stack);
|
||||
slot.mark_dirty();
|
||||
self.create_result(player.has_infinite_materials());
|
||||
self.send_content_updates();
|
||||
}
|
||||
|
||||
stack_left
|
||||
@ -162,35 +547,284 @@ impl ScreenHandler for AnvilScreenHandler {
|
||||
action_type: pumpkin_protocol::java::server::play::SlotActionType,
|
||||
player: &dyn InventoryPlayer,
|
||||
) {
|
||||
if slot_index == 2 {
|
||||
// Taking from output slot
|
||||
let result_slot = self.get_behaviour().slots[2].clone();
|
||||
if result_slot.has_stack() {
|
||||
let result_stack = result_slot.get_cloned_stack();
|
||||
if !result_stack.is_empty() {
|
||||
if player.experience_level() >= self.repair_cost as i32 || player.is_creative()
|
||||
{
|
||||
// Consume experience
|
||||
if !player.is_creative() {
|
||||
player.add_experience_levels(-(self.repair_cost as i32));
|
||||
}
|
||||
|
||||
// Consume inputs
|
||||
self.inventory.set_stack(0, ItemStack::EMPTY.clone());
|
||||
self.get_behaviour().slots[0].mark_dirty();
|
||||
} else {
|
||||
// Cancel click
|
||||
self.send_content_updates();
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
self.internal_on_slot_click(slot_index, button, action_type, player);
|
||||
if slot_index == 0 || slot_index == 1 || slot_index == 2 {
|
||||
self.update_result_slot();
|
||||
self.create_result(player.has_infinite_materials());
|
||||
self.send_content_updates();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::entity_equipment::EntityEquipment;
|
||||
use pumpkin_protocol::java::client::play::{
|
||||
CSetContainerContent, CSetContainerProperty, CSetContainerSlot, CSetCursorItem,
|
||||
CSetPlayerInventory, CSetSelectedSlot,
|
||||
};
|
||||
use pumpkin_world::inventory::SimpleInventory;
|
||||
use std::sync::Mutex;
|
||||
use std::sync::atomic::AtomicI32;
|
||||
|
||||
struct DummyPlayer {
|
||||
pub exp_level: AtomicI32,
|
||||
pub creative: bool,
|
||||
pub inventory: Arc<PlayerInventory>,
|
||||
}
|
||||
|
||||
impl DummyPlayer {
|
||||
fn new(exp_level: i32, creative: bool) -> Self {
|
||||
Self {
|
||||
exp_level: AtomicI32::new(exp_level),
|
||||
creative,
|
||||
inventory: Arc::new(PlayerInventory::new(
|
||||
Arc::new(Mutex::new(EntityEquipment::new())),
|
||||
Arc::new(rustc_hash::FxHashMap::default()),
|
||||
)),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl InventoryPlayer for DummyPlayer {
|
||||
fn as_any(&self) -> &dyn Any {
|
||||
self
|
||||
}
|
||||
|
||||
fn drop_item(&self, _item: ItemStack, _retain_ownership: bool) {}
|
||||
|
||||
fn is_creative(&self) -> bool {
|
||||
self.creative
|
||||
}
|
||||
|
||||
fn has_infinite_materials(&self) -> bool {
|
||||
self.creative
|
||||
}
|
||||
|
||||
fn experience_level(&self) -> i32 {
|
||||
self.exp_level.load(Ordering::Relaxed)
|
||||
}
|
||||
|
||||
fn add_experience_levels(&self, levels: i32) {
|
||||
self.exp_level.fetch_add(levels, Ordering::Relaxed);
|
||||
}
|
||||
|
||||
fn enchantment_seed(&self) -> i32 {
|
||||
0
|
||||
}
|
||||
|
||||
fn set_enchantment_seed(&self, _seed: i32) {}
|
||||
|
||||
fn get_inventory(&self) -> Arc<PlayerInventory> {
|
||||
self.inventory.clone()
|
||||
}
|
||||
|
||||
fn enqueue_inventory_packet(
|
||||
&self,
|
||||
_packet: &CSetContainerContent,
|
||||
_window_type: Option<WindowType>,
|
||||
) {
|
||||
}
|
||||
|
||||
fn enqueue_slot_packet(
|
||||
&self,
|
||||
_packet: &CSetContainerSlot,
|
||||
_window_type: Option<WindowType>,
|
||||
_total_slots: usize,
|
||||
) {
|
||||
}
|
||||
|
||||
fn enqueue_cursor_packet(&self, _packet: &CSetCursorItem) {}
|
||||
|
||||
fn enqueue_property_packet(&self, _packet: &CSetContainerProperty) {}
|
||||
|
||||
fn enqueue_slot_set_packet(&self, _packet: &CSetPlayerInventory) {}
|
||||
|
||||
fn enqueue_set_held_item_packet(&self, _packet: &CSetSelectedSlot) {}
|
||||
|
||||
fn enqueue_equipment_change(
|
||||
&self,
|
||||
_slot: &pumpkin_data::data_component_impl::EquipmentSlot,
|
||||
_stack: &ItemStack,
|
||||
) {
|
||||
}
|
||||
|
||||
fn award_experience(&self, _amount: i32) {}
|
||||
|
||||
fn increment_stat(
|
||||
&self,
|
||||
_category: pumpkin_data::statistic::StatisticCategory,
|
||||
_stat_id: i32,
|
||||
_amount: i32,
|
||||
) {
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn rename_item() {
|
||||
let player = DummyPlayer::new(10, false);
|
||||
let anvil_inv = Arc::new(SimpleInventory::new(3));
|
||||
let mut handler = AnvilScreenHandler::new(1, &player.inventory, anvil_inv.clone());
|
||||
|
||||
let sword = ItemStack::new(1, &Item::DIAMOND_SWORD);
|
||||
anvil_inv.set_stack(0, sword);
|
||||
|
||||
handler.set_item_name("Excalibur", false);
|
||||
|
||||
assert_eq!(handler.get_cost(), 1);
|
||||
let result = anvil_inv.get_stack(2);
|
||||
assert_eq!(result.get_hover_name(), "Excalibur");
|
||||
assert_eq!(result.get_repair_cost(), 0); // Only renaming does not increase repair cost
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn material_repair() {
|
||||
let player = DummyPlayer::new(10, false);
|
||||
let anvil_inv = Arc::new(SimpleInventory::new(3));
|
||||
let mut handler = AnvilScreenHandler::new(1, &player.inventory, anvil_inv.clone());
|
||||
|
||||
let mut damaged_sword = ItemStack::new(1, &Item::IRON_SWORD);
|
||||
let max_damage = damaged_sword.get_max_damage().unwrap();
|
||||
// 250 / 4 = 62 per ingot. 125 damage requires 3 ingots (62 + 62 + 1).
|
||||
damaged_sword.set_damage(max_damage / 2);
|
||||
anvil_inv.set_stack(0, damaged_sword);
|
||||
|
||||
let ingots = ItemStack::new(4, &Item::IRON_INGOT);
|
||||
anvil_inv.set_stack(1, ingots);
|
||||
|
||||
handler.create_result(false);
|
||||
|
||||
assert_eq!(handler.get_cost(), 3);
|
||||
let result = anvil_inv.get_stack(2);
|
||||
assert_eq!(result.get_damage(), 0);
|
||||
assert_eq!(result.get_repair_cost(), 1); // calculateIncreasedRepairCost(0) = 1
|
||||
assert_eq!(handler.repair_item_count_cost.load(Ordering::Relaxed), 3);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn tool_combine_durability() {
|
||||
let player = DummyPlayer::new(10, false);
|
||||
let anvil_inv = Arc::new(SimpleInventory::new(3));
|
||||
let mut handler = AnvilScreenHandler::new(1, &player.inventory, anvil_inv.clone());
|
||||
|
||||
let mut sword1 = ItemStack::new(1, &Item::DIAMOND_SWORD);
|
||||
let max_damage = sword1.get_max_damage().unwrap();
|
||||
sword1.set_damage(500);
|
||||
anvil_inv.set_stack(0, sword1);
|
||||
|
||||
let mut sword2 = ItemStack::new(1, &Item::DIAMOND_SWORD);
|
||||
sword2.set_damage(500);
|
||||
anvil_inv.set_stack(1, sword2);
|
||||
|
||||
handler.create_result(false);
|
||||
|
||||
assert_eq!(handler.get_cost(), 2);
|
||||
let result = anvil_inv.get_stack(2);
|
||||
// Durability combine: (max - 500) + (max - 500) + max * 12 / 100
|
||||
let expected_remaining = (max_damage - 500) + (max_damage - 500) + max_damage * 12 / 100;
|
||||
let expected_damage = (max_damage - expected_remaining).max(0);
|
||||
assert_eq!(result.get_damage(), expected_damage);
|
||||
assert_eq!(result.get_repair_cost(), 1);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn enchanted_book_combine() {
|
||||
let player = DummyPlayer::new(10, false);
|
||||
let anvil_inv = Arc::new(SimpleInventory::new(3));
|
||||
let mut handler = AnvilScreenHandler::new(1, &player.inventory, anvil_inv.clone());
|
||||
|
||||
let sword = ItemStack::new(1, &Item::DIAMOND_SWORD);
|
||||
anvil_inv.set_stack(0, sword);
|
||||
|
||||
let mut book = ItemStack::new(1, &Item::ENCHANTED_BOOK);
|
||||
let stored = StoredEnchantmentsImpl {
|
||||
enchantment: Cow::Owned(vec![(&Enchantment::SHARPNESS, 3)]),
|
||||
};
|
||||
book.set_data_component(stored);
|
||||
anvil_inv.set_stack(1, book);
|
||||
|
||||
handler.create_result(false);
|
||||
|
||||
// Sharpness anvil cost = 1. Fee using book = max(1, 1/2) = 1. Price = 1 * 3 = 3.
|
||||
assert_eq!(handler.get_cost(), 3);
|
||||
let result = anvil_inv.get_stack(2);
|
||||
let enchs = get_enchantments_for_crafting(&result);
|
||||
assert_eq!(enchs.len(), 1);
|
||||
assert_eq!(enchs[0].0.id, Enchantment::SHARPNESS.id);
|
||||
assert_eq!(enchs[0].1, 3);
|
||||
assert_eq!(result.get_repair_cost(), 1);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn enchantment_level_upgrade() {
|
||||
let player = DummyPlayer::new(10, false);
|
||||
let anvil_inv = Arc::new(SimpleInventory::new(3));
|
||||
let mut handler = AnvilScreenHandler::new(1, &player.inventory, anvil_inv.clone());
|
||||
|
||||
let mut sword1 = ItemStack::new(1, &Item::DIAMOND_SWORD);
|
||||
let ench1 = EnchantmentsImpl {
|
||||
enchantment: Cow::Owned(vec![(&Enchantment::SHARPNESS, 3)]),
|
||||
};
|
||||
sword1.set_data_component(ench1);
|
||||
anvil_inv.set_stack(0, sword1);
|
||||
|
||||
let mut sword2 = ItemStack::new(1, &Item::DIAMOND_SWORD);
|
||||
let ench2 = EnchantmentsImpl {
|
||||
enchantment: Cow::Owned(vec![(&Enchantment::SHARPNESS, 3)]),
|
||||
};
|
||||
sword2.set_data_component(ench2);
|
||||
anvil_inv.set_stack(1, sword2);
|
||||
|
||||
handler.create_result(false);
|
||||
|
||||
let result = anvil_inv.get_stack(2);
|
||||
let enchs = get_enchantments_for_crafting(&result);
|
||||
assert_eq!(enchs.len(), 1);
|
||||
assert_eq!(enchs[0].0.id, Enchantment::SHARPNESS.id);
|
||||
assert_eq!(enchs[0].1, 4);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn too_expensive_limit() {
|
||||
let player = DummyPlayer::new(50, false);
|
||||
let anvil_inv = Arc::new(SimpleInventory::new(3));
|
||||
let mut handler = AnvilScreenHandler::new(1, &player.inventory, anvil_inv.clone());
|
||||
|
||||
let mut sword1 = ItemStack::new(1, &Item::DIAMOND_SWORD);
|
||||
sword1.set_repair_cost(35);
|
||||
sword1.set_damage(500);
|
||||
anvil_inv.set_stack(0, sword1);
|
||||
|
||||
let mut sword2 = ItemStack::new(1, &Item::DIAMOND_SWORD);
|
||||
sword2.set_repair_cost(35);
|
||||
sword2.set_damage(500);
|
||||
anvil_inv.set_stack(1, sword2);
|
||||
|
||||
handler.create_result(false);
|
||||
|
||||
// Tax is 35 + 35 = 70. Combining damaged tools adds 2. Total = 72 >= 40.
|
||||
assert_eq!(handler.get_cost(), 72);
|
||||
assert!(anvil_inv.get_stack(2).is_empty());
|
||||
|
||||
// But in creative mode, result is available
|
||||
handler.create_result(true);
|
||||
assert!(!anvil_inv.get_stack(2).is_empty());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn rename_only_caps_at_39() {
|
||||
let player = DummyPlayer::new(50, false);
|
||||
let anvil_inv = Arc::new(SimpleInventory::new(3));
|
||||
let mut handler = AnvilScreenHandler::new(1, &player.inventory, anvil_inv.clone());
|
||||
|
||||
let mut sword = ItemStack::new(1, &Item::DIAMOND_SWORD);
|
||||
sword.set_repair_cost(45);
|
||||
anvil_inv.set_stack(0, sword);
|
||||
|
||||
handler.set_item_name("Legendary", false);
|
||||
|
||||
// Renaming only is capped at 39 even with high repair cost
|
||||
assert_eq!(handler.get_cost(), 39);
|
||||
assert!(!anvil_inv.get_stack(2).is_empty());
|
||||
}
|
||||
}
|
||||
|
||||
@ -208,6 +208,9 @@ pub trait InventoryPlayer: Send + Sync {
|
||||
|
||||
/// Closes the player's current handled screen.
|
||||
fn close_screen_handler(&self) {}
|
||||
|
||||
/// Performs anvil block damage and plays anvil sound events.
|
||||
fn use_anvil(&self) {}
|
||||
}
|
||||
|
||||
/// Gives a stack to the player or drops it if inventory is full.
|
||||
|
||||
@ -13,12 +13,14 @@ use pumpkin_data::data_component_impl::{
|
||||
HorseVariantImpl, IDSet, IDSetContent, IdOr, ItemModelImpl, ItemNameImpl, LlamaVariantImpl,
|
||||
LoreImpl, MapIdImpl, MaxStackSizeImpl, MooshroomVariantImpl, PaintingVariantImpl,
|
||||
ParrotVariantImpl, PigSoundVariantImpl, PigVariantImpl, PotionContentsImpl, RabbitVariantImpl,
|
||||
SalmonSizeImpl, SheepColorImpl, ShulkerColorImpl, SoundEvent, StatusEffectInstance,
|
||||
StoredEnchantmentsImpl, SuspiciousStewEffect, SuspiciousStewEffectsImpl,
|
||||
Rarity, RarityImpl, RepairCostImpl, RepairableImpl, SalmonSizeImpl, SheepColorImpl,
|
||||
ShulkerColorImpl, SoundEvent, StatusEffectInstance, StoredEnchantmentsImpl,
|
||||
SuspiciousStewEffect, SuspiciousStewEffectsImpl, SwingAnimationImpl, SwingAnimationType,
|
||||
TropicalFishBaseColorImpl, TropicalFishPatternColorImpl, TropicalFishPatternImpl,
|
||||
UnbreakableImpl, UseCooldownImpl, VillagerVariantImpl, WolfCollarImpl, WolfSoundVariantImpl,
|
||||
WolfVariantImpl, ZombieNautilusVariantImpl, get,
|
||||
};
|
||||
|
||||
use pumpkin_data::effect::StatusEffect;
|
||||
use pumpkin_data::entity::EntityType;
|
||||
use pumpkin_data::sound::Sound;
|
||||
@ -262,6 +264,16 @@ impl DataComponentCodec<Self> for DamageImpl {
|
||||
}
|
||||
}
|
||||
|
||||
impl DataComponentCodec<Self> for RepairCostImpl {
|
||||
fn serialize(&self, seq: &mut impl NetworkWriteExt) -> Result<(), WritingError> {
|
||||
seq.write_var_int(&VarInt::from(self.cost))
|
||||
}
|
||||
fn deserialize(seq: &mut impl NetworkReadExt) -> Result<Self, ReadingError> {
|
||||
let cost = seq.get_var_int()?.0;
|
||||
Ok(Self { cost })
|
||||
}
|
||||
}
|
||||
|
||||
impl DataComponentCodec<Self> for EnchantmentsImpl {
|
||||
fn serialize(&self, seq: &mut impl NetworkWriteExt) -> Result<(), WritingError> {
|
||||
seq.write_var_int(&VarInt::from(self.enchantment.len() as i32))?;
|
||||
@ -860,6 +872,50 @@ impl DataComponentCodec<Self> for StoredEnchantmentsImpl {
|
||||
}
|
||||
}
|
||||
|
||||
impl DataComponentCodec<Self> for RepairableImpl {
|
||||
fn serialize(&self, seq: &mut impl NetworkWriteExt) -> Result<(), WritingError> {
|
||||
serialize_idset(&self.items, seq)
|
||||
}
|
||||
|
||||
fn deserialize(seq: &mut impl NetworkReadExt) -> Result<Self, ReadingError> {
|
||||
Ok(Self {
|
||||
items: deserialize_idset(seq)?,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl DataComponentCodec<Self> for SwingAnimationImpl {
|
||||
fn serialize(&self, seq: &mut impl NetworkWriteExt) -> Result<(), WritingError> {
|
||||
seq.write_var_int(&VarInt::from(self.animation_type.to_id()))?;
|
||||
seq.write_var_int(&VarInt::from(self.duration))
|
||||
}
|
||||
|
||||
fn deserialize(seq: &mut impl NetworkReadExt) -> Result<Self, ReadingError> {
|
||||
let type_id = seq.get_var_int()?.0;
|
||||
let animation_type = SwingAnimationType::from_id(type_id).ok_or_else(|| {
|
||||
ReadingError::Message(format!("Invalid SwingAnimationType id {type_id}"))
|
||||
})?;
|
||||
let duration = seq.get_var_int()?.0;
|
||||
Ok(Self {
|
||||
animation_type,
|
||||
duration,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl DataComponentCodec<Self> for RarityImpl {
|
||||
fn serialize(&self, seq: &mut impl NetworkWriteExt) -> Result<(), WritingError> {
|
||||
seq.write_var_int(&VarInt::from(self.rarity.to_id()))
|
||||
}
|
||||
|
||||
fn deserialize(seq: &mut impl NetworkReadExt) -> Result<Self, ReadingError> {
|
||||
let id = seq.get_var_int()?.0;
|
||||
let rarity = Rarity::from_id(id)
|
||||
.ok_or_else(|| ReadingError::Message(format!("Invalid Rarity id {id}")))?;
|
||||
Ok(Self { rarity })
|
||||
}
|
||||
}
|
||||
|
||||
pub fn deserialize(
|
||||
id: DataComponent,
|
||||
seq: &mut impl NetworkReadExt,
|
||||
@ -885,6 +941,10 @@ pub fn deserialize(
|
||||
DataComponent::Consumable => Ok(ConsumableImpl::deserialize(seq)?.to_dyn()),
|
||||
DataComponent::Equippable => Ok(EquippableImpl::deserialize(seq)?.to_dyn()),
|
||||
DataComponent::StoredEnchantments => Ok(StoredEnchantmentsImpl::deserialize(seq)?.to_dyn()),
|
||||
DataComponent::RepairCost => Ok(RepairCostImpl::deserialize(seq)?.to_dyn()),
|
||||
DataComponent::Repairable => Ok(RepairableImpl::deserialize(seq)?.to_dyn()),
|
||||
DataComponent::SwingAnimation => Ok(SwingAnimationImpl::deserialize(seq)?.to_dyn()),
|
||||
DataComponent::Rarity => Ok(RarityImpl::deserialize(seq)?.to_dyn()),
|
||||
DataComponent::UseCooldown => Ok(UseCooldownImpl::deserialize(seq)?.to_dyn()),
|
||||
DataComponent::MapId => Ok(MapIdImpl::deserialize(seq)?.to_dyn()),
|
||||
DataComponent::BundleContents => Ok(BundleContentsImpl::deserialize(seq)?.to_dyn()),
|
||||
@ -916,6 +976,10 @@ pub fn serialize(
|
||||
DataComponent::Consumable => get::<ConsumableImpl>(value).serialize(seq),
|
||||
DataComponent::Equippable => get::<EquippableImpl>(value).serialize(seq),
|
||||
DataComponent::StoredEnchantments => get::<StoredEnchantmentsImpl>(value).serialize(seq),
|
||||
DataComponent::RepairCost => get::<RepairCostImpl>(value).serialize(seq),
|
||||
DataComponent::Repairable => get::<RepairableImpl>(value).serialize(seq),
|
||||
DataComponent::SwingAnimation => get::<SwingAnimationImpl>(value).serialize(seq),
|
||||
DataComponent::Rarity => get::<RarityImpl>(value).serialize(seq),
|
||||
DataComponent::UseCooldown => get::<UseCooldownImpl>(value).serialize(seq),
|
||||
DataComponent::MapId => get::<MapIdImpl>(value).serialize(seq),
|
||||
DataComponent::BundleContents => get::<BundleContentsImpl>(value).serialize(seq),
|
||||
|
||||
@ -12,6 +12,7 @@ use pumpkin_util::version::JavaMinecraftVersion;
|
||||
|
||||
use crate::{
|
||||
ClientPacket, VarInt,
|
||||
codec::var_long::VarLong,
|
||||
ser::{NetworkWriteExt, WritingError},
|
||||
};
|
||||
|
||||
@ -65,8 +66,7 @@ impl ClientPacket for CSetEntityMetadata {
|
||||
mut write: impl Write,
|
||||
version: &JavaMinecraftVersion,
|
||||
) -> Result<(), WritingError> {
|
||||
// 1. Entity ID
|
||||
if *version <= JavaMinecraftVersion::V_1_7_6 {
|
||||
if *version < JavaMinecraftVersion::V_1_8 {
|
||||
write.write_i32_be(self.entity_id.0)?;
|
||||
} else {
|
||||
write.write_var_int(&self.entity_id)?;
|
||||
@ -141,7 +141,9 @@ impl<T> Metadata<T> {
|
||||
writer.write_u8(resolved_index)?;
|
||||
writer.write_var_int(&VarInt(remapped_type_id))?;
|
||||
|
||||
if self.r#type == MetaDataType::BLOCK_STATE {
|
||||
if self.r#type == MetaDataType::BLOCK_STATE
|
||||
|| self.r#type == MetaDataType::OPTIONAL_BLOCK_STATE
|
||||
{
|
||||
let mut serialized_value = Vec::new();
|
||||
self.value.write_metadata(&mut serialized_value, version)?;
|
||||
|
||||
@ -152,7 +154,11 @@ impl<T> Metadata<T> {
|
||||
let remapped_state = u16::try_from(decoded_state.0).map_or(decoded_state, |state_id| {
|
||||
VarInt(i32::from(remap_block_state_for_version(state_id, *version)))
|
||||
});
|
||||
writer.write_var_int(&remapped_state)?;
|
||||
if *version >= JavaMinecraftVersion::V_1_9 {
|
||||
writer.write_var_int(&remapped_state)?;
|
||||
} else {
|
||||
writer.write_i32(remapped_state.0)?;
|
||||
}
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
@ -257,9 +263,13 @@ impl MetadataSerializer for i32 {
|
||||
fn write_metadata(
|
||||
&self,
|
||||
writer: &mut impl std::io::Write,
|
||||
_version: &JavaMinecraftVersion,
|
||||
version: &JavaMinecraftVersion,
|
||||
) -> Result<(), WritingError> {
|
||||
writer.write_i32(*self)
|
||||
if *version >= JavaMinecraftVersion::V_1_9 {
|
||||
writer.write_var_int(&VarInt(*self))
|
||||
} else {
|
||||
writer.write_i32(*self)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -267,9 +277,55 @@ impl MetadataSerializer for u32 {
|
||||
fn write_metadata(
|
||||
&self,
|
||||
writer: &mut impl std::io::Write,
|
||||
_version: &JavaMinecraftVersion,
|
||||
version: &JavaMinecraftVersion,
|
||||
) -> Result<(), WritingError> {
|
||||
writer.write_u32(*self)
|
||||
if *version >= JavaMinecraftVersion::V_1_9 {
|
||||
writer.write_var_int(&VarInt(*self as i32))
|
||||
} else {
|
||||
writer.write_u32(*self)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl MetadataSerializer for i64 {
|
||||
fn write_metadata(
|
||||
&self,
|
||||
writer: &mut impl std::io::Write,
|
||||
version: &JavaMinecraftVersion,
|
||||
) -> Result<(), WritingError> {
|
||||
if *version >= JavaMinecraftVersion::V_1_9 {
|
||||
writer.write_var_long(&VarLong(*self))
|
||||
} else {
|
||||
writer.write_i64_be(*self)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl MetadataSerializer for u64 {
|
||||
fn write_metadata(
|
||||
&self,
|
||||
writer: &mut impl std::io::Write,
|
||||
version: &JavaMinecraftVersion,
|
||||
) -> Result<(), WritingError> {
|
||||
if *version >= JavaMinecraftVersion::V_1_9 {
|
||||
writer.write_var_long(&VarLong(*self as i64))
|
||||
} else {
|
||||
writer.write_u64_be(*self)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl MetadataSerializer for VarLong {
|
||||
fn write_metadata(
|
||||
&self,
|
||||
writer: &mut impl std::io::Write,
|
||||
version: &JavaMinecraftVersion,
|
||||
) -> Result<(), WritingError> {
|
||||
if *version >= JavaMinecraftVersion::V_1_9 {
|
||||
writer.write_var_long(self)
|
||||
} else {
|
||||
writer.write_i64_be(self.0)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -360,13 +416,44 @@ impl MetadataSerializer for Option<String> {
|
||||
}
|
||||
}
|
||||
|
||||
impl MetadataSerializer for pumpkin_util::math::vector3::Vector3<f32> {
|
||||
fn write_metadata(
|
||||
&self,
|
||||
writer: &mut impl std::io::Write,
|
||||
_version: &JavaMinecraftVersion,
|
||||
) -> Result<(), WritingError> {
|
||||
writer.write_f32(self.x)?;
|
||||
writer.write_f32(self.y)?;
|
||||
writer.write_f32(self.z)
|
||||
}
|
||||
}
|
||||
|
||||
impl MetadataSerializer for [f32; 4] {
|
||||
fn write_metadata(
|
||||
&self,
|
||||
writer: &mut impl std::io::Write,
|
||||
_version: &JavaMinecraftVersion,
|
||||
) -> Result<(), WritingError> {
|
||||
writer.write_f32(self[0])?;
|
||||
writer.write_f32(self[1])?;
|
||||
writer.write_f32(self[2])?;
|
||||
writer.write_f32(self[3])
|
||||
}
|
||||
}
|
||||
|
||||
impl MetadataSerializer for pumpkin_util::math::position::BlockPos {
|
||||
fn write_metadata(
|
||||
&self,
|
||||
writer: &mut impl std::io::Write,
|
||||
version: &JavaMinecraftVersion,
|
||||
) -> Result<(), WritingError> {
|
||||
writer.write_block_pos(self, version)
|
||||
if *version >= JavaMinecraftVersion::V_1_9 {
|
||||
writer.write_block_pos(self, version)
|
||||
} else {
|
||||
writer.write_i32(self.0.x)?;
|
||||
writer.write_i32(self.0.y)?;
|
||||
writer.write_i32(self.0.z)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -378,7 +465,7 @@ impl MetadataSerializer for Option<pumpkin_util::math::position::BlockPos> {
|
||||
) -> Result<(), WritingError> {
|
||||
if let Some(pos) = self {
|
||||
writer.write_bool(true)?;
|
||||
writer.write_block_pos(pos, version)?;
|
||||
pos.write_metadata(writer, version)?;
|
||||
} else {
|
||||
writer.write_bool(false)?;
|
||||
}
|
||||
@ -390,10 +477,14 @@ impl MetadataSerializer for crate::codec::optional_int::OptionalInt {
|
||||
fn write_metadata(
|
||||
&self,
|
||||
writer: &mut impl std::io::Write,
|
||||
_version: &JavaMinecraftVersion,
|
||||
version: &JavaMinecraftVersion,
|
||||
) -> Result<(), WritingError> {
|
||||
let val = self.0.map_or(0, |id| id + 1);
|
||||
writer.write_var_int(&VarInt(val))
|
||||
if *version >= JavaMinecraftVersion::V_1_9 {
|
||||
writer.write_var_int(&VarInt(val))
|
||||
} else {
|
||||
writer.write_i32(val)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -490,4 +581,38 @@ mod tests {
|
||||
assert_eq!(particle_id, VarInt(29));
|
||||
assert_eq!(data, [0x12, 0x34, 0x56, 0x78]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn int_metadata_encodes_varint_for_1_9_plus_and_i32_for_legacy() {
|
||||
let mut buf_modern = Vec::new();
|
||||
(-1i32)
|
||||
.write_metadata(&mut buf_modern, &JavaMinecraftVersion::V_1_21)
|
||||
.unwrap();
|
||||
// -1 as VarInt is 5 bytes: 0xFF, 0xFF, 0xFF, 0xFF, 0x0F
|
||||
assert_eq!(buf_modern, vec![0xFF, 0xFF, 0xFF, 0xFF, 0x0F]);
|
||||
|
||||
let mut buf_legacy = Vec::new();
|
||||
(-1i32)
|
||||
.write_metadata(&mut buf_legacy, &JavaMinecraftVersion::V_1_8)
|
||||
.unwrap();
|
||||
// -1 as i32 BE is 4 bytes: 0xFF, 0xFF, 0xFF, 0xFF
|
||||
assert_eq!(buf_legacy, vec![0xFF, 0xFF, 0xFF, 0xFF]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn block_pos_metadata_encodes_packed_for_1_9_plus_and_three_ints_for_legacy() {
|
||||
let pos = pumpkin_util::math::position::BlockPos(
|
||||
pumpkin_util::math::vector3::Vector3::new(10, 20, 30),
|
||||
);
|
||||
|
||||
let mut buf_modern = Vec::new();
|
||||
pos.write_metadata(&mut buf_modern, &JavaMinecraftVersion::V_1_21)
|
||||
.unwrap();
|
||||
assert_eq!(buf_modern.len(), 8); // packed i64
|
||||
|
||||
let mut buf_legacy = Vec::new();
|
||||
pos.write_metadata(&mut buf_legacy, &JavaMinecraftVersion::V_1_8)
|
||||
.unwrap();
|
||||
assert_eq!(buf_legacy.len(), 12); // 3 * i32 (12 bytes)
|
||||
}
|
||||
}
|
||||
|
||||
@ -8,7 +8,7 @@ use pumpkin_data::tag::Block::MINECRAFT_LEAVES;
|
||||
use pumpkin_data::{Block, BlockState, BlockStateId};
|
||||
use pumpkin_nbt::compound::NbtCompound;
|
||||
use pumpkin_util::math::position::BlockPos;
|
||||
use rustc_hash::FxHashMap;
|
||||
use rustc_hash::{FxHashMap, FxHashSet};
|
||||
|
||||
use std::sync::RwLock;
|
||||
use std::sync::atomic::AtomicBool;
|
||||
@ -651,6 +651,117 @@ impl ChunkData {
|
||||
old
|
||||
}
|
||||
|
||||
/// Sets multiple blocks in the chunk at absolute Y coordinates in a single batch.
|
||||
///
|
||||
/// This acquires section write locks and heightmap locks once across all updates,
|
||||
/// significantly speeding up bulk block modifications.
|
||||
///
|
||||
/// Returns a list of `(relative_x, y, relative_z, replaced_block_state_id)`.
|
||||
pub fn set_blocks_batch(
|
||||
&self,
|
||||
updates: impl IntoIterator<Item = (usize, i32, usize, BlockStateId)>,
|
||||
) -> Vec<(usize, i32, usize, BlockStateId)> {
|
||||
let min_y = self.section.min_y;
|
||||
let mut results = Vec::new();
|
||||
|
||||
let mut sections = self
|
||||
.section
|
||||
.block_sections
|
||||
.write()
|
||||
.unwrap_or_else(std::sync::PoisonError::into_inner);
|
||||
let mut random_tick_sections_guard = self
|
||||
.section
|
||||
.random_tick_sections
|
||||
.write()
|
||||
.unwrap_or_else(std::sync::PoisonError::into_inner);
|
||||
|
||||
let mut changed_columns = FxHashSet::default();
|
||||
let mut modified = false;
|
||||
|
||||
for (rel_x, y, rel_z, new_state_id) in updates {
|
||||
let y_rel = y - min_y;
|
||||
if y_rel < 0 {
|
||||
continue;
|
||||
}
|
||||
let rel_y = y_rel as usize;
|
||||
let section_index = rel_y / BlockPalette::SIZE;
|
||||
let sub_y = rel_y % BlockPalette::SIZE;
|
||||
|
||||
if let Some(section) = sections.get_mut(section_index) {
|
||||
let replaced_id = section.set(rel_x, sub_y, rel_z, new_state_id);
|
||||
if replaced_id != new_state_id {
|
||||
modified = true;
|
||||
changed_columns.insert((rel_x, rel_z));
|
||||
if (has_random_ticks(new_state_id) || has_random_ticking_fluid(new_state_id))
|
||||
&& random_tick_sections_guard.is_none()
|
||||
{
|
||||
let new_cache = vec![RandomTickSectionCache::default(); self.section.count]
|
||||
.into_boxed_slice();
|
||||
*random_tick_sections_guard = Some(new_cache);
|
||||
}
|
||||
|
||||
if let Some(random_tick_sections) = random_tick_sections_guard.as_mut() {
|
||||
let random_tick_cache = &mut random_tick_sections[section_index];
|
||||
if has_random_ticks(replaced_id) {
|
||||
random_tick_cache.random_ticking_block_count = random_tick_cache
|
||||
.random_ticking_block_count
|
||||
.saturating_sub(1);
|
||||
}
|
||||
if has_random_ticking_fluid(replaced_id) {
|
||||
random_tick_cache.random_ticking_fluid_count = random_tick_cache
|
||||
.random_ticking_fluid_count
|
||||
.saturating_sub(1);
|
||||
}
|
||||
if has_random_ticks(new_state_id) {
|
||||
random_tick_cache.random_ticking_block_count = random_tick_cache
|
||||
.random_ticking_block_count
|
||||
.saturating_add(1);
|
||||
}
|
||||
if has_random_ticking_fluid(new_state_id) {
|
||||
random_tick_cache.random_ticking_fluid_count = random_tick_cache
|
||||
.random_ticking_fluid_count
|
||||
.saturating_add(1);
|
||||
}
|
||||
|
||||
let mut mask = self
|
||||
.section
|
||||
.randomly_ticking_mask
|
||||
.load(std::sync::atomic::Ordering::Relaxed);
|
||||
if random_tick_cache.is_randomly_ticking() {
|
||||
mask |= 1 << section_index;
|
||||
} else {
|
||||
mask &= !(1 << section_index);
|
||||
}
|
||||
self.section
|
||||
.randomly_ticking_mask
|
||||
.store(mask, std::sync::atomic::Ordering::Relaxed);
|
||||
}
|
||||
}
|
||||
results.push((rel_x, y, rel_z, replaced_id));
|
||||
}
|
||||
}
|
||||
|
||||
if modified {
|
||||
self.dirty.store(true, std::sync::atomic::Ordering::Relaxed);
|
||||
}
|
||||
|
||||
drop(sections);
|
||||
drop(random_tick_sections_guard);
|
||||
|
||||
if !changed_columns.is_empty() {
|
||||
let mut heightmap = self
|
||||
.heightmap
|
||||
.lock()
|
||||
.unwrap_or_else(std::sync::PoisonError::into_inner);
|
||||
let highest_non_empty_subchunk = self.get_highest_non_empty_subchunk();
|
||||
for (x, z) in changed_columns {
|
||||
self.populate_heightmaps(&mut heightmap, highest_non_empty_subchunk, x, z);
|
||||
}
|
||||
}
|
||||
|
||||
results
|
||||
}
|
||||
|
||||
fn update_heightmap(
|
||||
&self,
|
||||
relative_x: usize,
|
||||
|
||||
@ -5,9 +5,9 @@ use crate::block::{
|
||||
PlacedArgs,
|
||||
};
|
||||
|
||||
use pumpkin_data::BlockStateId;
|
||||
use pumpkin_data::block_properties::{BlockProperties, WallTorchLikeProperties};
|
||||
use pumpkin_data::translation;
|
||||
use pumpkin_data::{Block, BlockStateId};
|
||||
use pumpkin_inventory::anvil::AnvilScreenHandler;
|
||||
use pumpkin_inventory::player::player_inventory::PlayerInventory;
|
||||
use pumpkin_inventory::screen_handler::{
|
||||
@ -22,6 +22,22 @@ use std::sync::Mutex;
|
||||
#[pumpkin_block_from_tag("minecraft:anvil")]
|
||||
pub struct AnvilBlock;
|
||||
|
||||
impl AnvilBlock {
|
||||
#[must_use]
|
||||
pub fn damage(state_id: BlockStateId) -> Option<BlockStateId> {
|
||||
let block = Block::from_state_id(state_id);
|
||||
let next_block = if block == &Block::ANVIL {
|
||||
&Block::CHIPPED_ANVIL
|
||||
} else if block == &Block::CHIPPED_ANVIL {
|
||||
&Block::DAMAGED_ANVIL
|
||||
} else {
|
||||
return None;
|
||||
};
|
||||
let props = WallTorchLikeProperties::from_state_id(state_id, block);
|
||||
Some(props.to_state_id(next_block))
|
||||
}
|
||||
}
|
||||
|
||||
impl BlockBehaviour for AnvilBlock {
|
||||
fn normal_use(&self, args: NormalUseArgs<'_>) -> BlockActionResult {
|
||||
args.player.increment_stat(
|
||||
|
||||
@ -32,7 +32,7 @@ impl BlockBehaviour for CoralBlock {
|
||||
};
|
||||
let dead_block_state_id = dead_block.default_state.id;
|
||||
args.world
|
||||
.set_block_state(args.position, dead_block_state_id, BlockFlags::empty());
|
||||
.set_block_state(args.position, dead_block_state_id, BlockFlags::NOTIFY_ALL);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -126,7 +126,7 @@ impl BlockBehaviour for CoralFanBlock {
|
||||
};
|
||||
|
||||
args.world
|
||||
.set_block_state(args.position, dead_block_state_id, BlockFlags::empty());
|
||||
.set_block_state(args.position, dead_block_state_id, BlockFlags::NOTIFY_ALL);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -46,7 +46,7 @@ impl BlockBehaviour for CoralPlantBlock {
|
||||
props.to_state_id(get_dead_type(args.block.id).unwrap_or_default().to_block())
|
||||
};
|
||||
args.world
|
||||
.set_block_state(args.position, dead_block_state_id, BlockFlags::empty());
|
||||
.set_block_state(args.position, dead_block_state_id, BlockFlags::NOTIFY_ALL);
|
||||
}
|
||||
}
|
||||
fn can_place_at(&self, args: CanPlaceAtArgs<'_>) -> bool {
|
||||
|
||||
@ -163,13 +163,9 @@ impl FireBlock {
|
||||
let mut fire_props = FireProperties::from_state_id(state_id, &Block::FIRE);
|
||||
fire_props.age = new_age;
|
||||
let new_state_id = fire_props.to_state_id(&Block::FIRE);
|
||||
world.set_block_state(pos, new_state_id, BlockFlags::NOTIFY_NEIGHBORS);
|
||||
world.set_block_state(pos, new_state_id, BlockFlags::NOTIFY_ALL);
|
||||
} else {
|
||||
world.set_block_state(
|
||||
pos,
|
||||
Block::AIR.default_state.id,
|
||||
BlockFlags::NOTIFY_NEIGHBORS,
|
||||
);
|
||||
world.set_block_state(pos, Block::AIR.default_state.id, BlockFlags::NOTIFY_ALL);
|
||||
}
|
||||
|
||||
if old_block == &Block::TNT {
|
||||
@ -308,7 +304,7 @@ impl BlockBehaviour for FireBlock {
|
||||
if new_age != age {
|
||||
fire_props.age = new_age;
|
||||
let new_state_id = fire_props.to_state_id(&Block::FIRE);
|
||||
world.set_block_state(pos, new_state_id, BlockFlags::NOTIFY_NEIGHBORS);
|
||||
world.set_block_state(pos, new_state_id, BlockFlags::NOTIFY_ALL);
|
||||
}
|
||||
|
||||
if !infiniburn {
|
||||
@ -316,11 +312,7 @@ impl BlockBehaviour for FireBlock {
|
||||
if !Self::are_blocks_around_flammable(world.as_ref(), pos) {
|
||||
let block_below_state = world.get_block_state(&pos.down());
|
||||
if !block_below_state.is_side_solid(BlockDirection::Up) || new_age > 3 {
|
||||
world.set_block_state(
|
||||
pos,
|
||||
Block::AIR.default_state.id,
|
||||
BlockFlags::NOTIFY_NEIGHBORS,
|
||||
);
|
||||
world.set_block_state(pos, Block::AIR.default_state.id, BlockFlags::NOTIFY_ALL);
|
||||
return;
|
||||
}
|
||||
}
|
||||
@ -330,11 +322,7 @@ impl BlockBehaviour for FireBlock {
|
||||
&& rand::rng().random_range(0..4) == 0
|
||||
&& !Self::is_flammable(world.get_block_state(&pos.down()))
|
||||
{
|
||||
world.set_block_state(
|
||||
pos,
|
||||
Block::AIR.default_state.id,
|
||||
BlockFlags::NOTIFY_NEIGHBORS,
|
||||
);
|
||||
world.set_block_state(pos, Block::AIR.default_state.id, BlockFlags::NOTIFY_ALL);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
@ -221,7 +221,7 @@ impl PistonBlock {
|
||||
world.set_block_state(
|
||||
pos,
|
||||
props.to_state_id(&Block::MOVING_PISTON),
|
||||
BlockFlags::FORCE_STATE,
|
||||
BlockFlags::NOTIFY_ALL | BlockFlags::FORCE_STATE,
|
||||
);
|
||||
|
||||
let mut props = PistonProps::default(block);
|
||||
@ -242,7 +242,7 @@ impl PistonBlock {
|
||||
world.set_block_state(
|
||||
&extended_pos,
|
||||
Block::AIR.default_state.id,
|
||||
BlockFlags::FORCE_STATE,
|
||||
BlockFlags::NOTIFY_ALL | BlockFlags::FORCE_STATE,
|
||||
);
|
||||
|
||||
world.update_neighbors(pos, None);
|
||||
@ -415,7 +415,11 @@ fn move_piston(
|
||||
props.facing = dir.to_facing();
|
||||
let state = props.to_state_id(&Block::MOVING_PISTON);
|
||||
|
||||
world.set_block_state(&target_pos, state, BlockFlags::MOVED);
|
||||
world.set_block_state(
|
||||
&target_pos,
|
||||
state,
|
||||
BlockFlags::NOTIFY_ALL | BlockFlags::MOVED,
|
||||
);
|
||||
|
||||
if let Some(moved_state) = moved_block_states.get(moved_blocks.len() - 1 - index) {
|
||||
world.add_block_entity(Arc::new(PistonBlockEntity {
|
||||
@ -444,7 +448,7 @@ fn move_piston(
|
||||
world.set_block_state(
|
||||
&extended_pos,
|
||||
props.to_state_id(&Block::MOVING_PISTON),
|
||||
BlockFlags::MOVED,
|
||||
BlockFlags::NOTIFY_ALL | BlockFlags::MOVED,
|
||||
);
|
||||
let mut props = PistonHeadLikeProperties::default(&Block::PISTON_HEAD);
|
||||
props.facing = dir.to_facing();
|
||||
|
||||
@ -72,7 +72,7 @@ trait CropBlockBase: PlantBlockBase {
|
||||
let f = get_available_moisture(world, pos, block);
|
||||
if rand::rng().random_range(0..=(25.0 / f).floor() as i64) == 0 {
|
||||
let new_state_id = self.state_with_age(block, state, age + 1);
|
||||
world.set_block_state(pos, new_state_id, BlockFlags::NOTIFY_NEIGHBORS);
|
||||
world.set_block_state(pos, new_state_id, BlockFlags::NOTIFY_LISTENERS);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -23,7 +23,7 @@ impl BlockBehaviour for SugarCaneBlock {
|
||||
fn on_scheduled_tick(&self, args: OnScheduledTickArgs<'_>) {
|
||||
if !can_place_at(args.world.as_ref(), args.position) {
|
||||
args.world
|
||||
.break_block(args.position, None, BlockFlags::empty());
|
||||
.break_block(args.position, None, BlockFlags::NOTIFY_ALL);
|
||||
}
|
||||
}
|
||||
|
||||
@ -36,19 +36,19 @@ impl BlockBehaviour for SugarCaneBlock {
|
||||
let age = CactusLikeProperties::from_state_id(state_id, args.block).age;
|
||||
if age == 15 {
|
||||
args.world
|
||||
.set_block_state(&args.position.up(), state_id, BlockFlags::empty());
|
||||
.set_block_state(&args.position.up(), state_id, BlockFlags::NOTIFY_ALL);
|
||||
let props = CactusLikeProperties { age: 0 };
|
||||
args.world.set_block_state(
|
||||
args.position,
|
||||
props.to_state_id(args.block),
|
||||
BlockFlags::empty(),
|
||||
BlockFlags::NOTIFY_LISTENERS,
|
||||
);
|
||||
} else {
|
||||
let props = CactusLikeProperties { age: age + 1 };
|
||||
args.world.set_block_state(
|
||||
args.position,
|
||||
props.to_state_id(args.block),
|
||||
BlockFlags::empty(),
|
||||
BlockFlags::NOTIFY_LISTENERS,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@ -243,7 +243,7 @@ impl ComparatorBlock {
|
||||
};
|
||||
|
||||
let state_id = props.to_state_id(block);
|
||||
world.set_block_state(&block_pos, state_id, BlockFlags::empty());
|
||||
world.set_block_state(&block_pos, state_id, BlockFlags::NOTIFY_ALL);
|
||||
|
||||
self.update(world, block_pos, BlockState::from_id(state_id), block);
|
||||
}
|
||||
|
||||
@ -341,6 +341,7 @@ impl DispenserBlock {
|
||||
);
|
||||
let arrow =
|
||||
ArrowEntity::new_with_item(arrow_entity, None, &projectile, ArrowPickup::Allowed);
|
||||
arrow.apply_on_projectile_spawned(&projectile);
|
||||
|
||||
arrow.set_velocity(
|
||||
facing.x,
|
||||
@ -572,15 +573,18 @@ impl DispenserBlock {
|
||||
&EntityType::SMALL_FIREBALL,
|
||||
);
|
||||
let fireball = SmallFireballEntity::new(entity);
|
||||
// Vanilla aims fire charges straight along the facing axis, without the +0.1 Y bias
|
||||
// other projectiles get.
|
||||
let facing = to_normal(ctx.facing);
|
||||
fireball.thrown.set_velocity(
|
||||
facing.x,
|
||||
facing.y,
|
||||
facing.z,
|
||||
Self::FIREBALL_PROJECTILE_POWER,
|
||||
Self::FIREBALL_PROJECTILE_UNCERTAINTY,
|
||||
let dir = Vector3::new(
|
||||
triangle(&mut rng(), facing.x, 0.114_850_000_000_000_01),
|
||||
triangle(&mut rng(), facing.y, 0.114_850_000_000_000_01),
|
||||
triangle(&mut rng(), facing.z, 0.114_850_000_000_000_01),
|
||||
)
|
||||
.normalize();
|
||||
fireball.get_entity().set_velocity(dir);
|
||||
let len = dir.horizontal_length();
|
||||
fireball.get_entity().set_rotation(
|
||||
dir.x.atan2(dir.z) as f32 * 57.295_776,
|
||||
dir.y.atan2(len) as f32 * 57.295_776,
|
||||
);
|
||||
Self::finish_projectile_launch(ctx, Arc::new(fireball), WorldEvent::SoundBlazeFireball);
|
||||
}
|
||||
@ -721,8 +725,7 @@ impl DispenserBlock {
|
||||
let front_block = ctx.world.get_block(&front);
|
||||
|
||||
let ignited = if front_block == &Block::TNT {
|
||||
TNTBlock::prime(ctx.world, &front);
|
||||
true
|
||||
TNTBlock::prime(ctx.world, &front)
|
||||
} else {
|
||||
Ignition::ignite_block(
|
||||
|world: Arc<World>, pos: BlockPos, new_state_id: BlockStateId| {
|
||||
|
||||
@ -289,7 +289,7 @@ pub fn update_power_strength(world: &Arc<World>, pos: &BlockPos) {
|
||||
wire.power = target_strength;
|
||||
let new_state_id = wire.to_state_id(&Block::REDSTONE_WIRE);
|
||||
|
||||
world.set_block_state(pos, new_state_id, BlockFlags::empty());
|
||||
world.set_block_state(pos, new_state_id, BlockFlags::NOTIFY_LISTENERS);
|
||||
|
||||
let mut to_update = Vec::with_capacity(7);
|
||||
to_update.push(*pos);
|
||||
|
||||
@ -219,7 +219,7 @@ impl RepeaterBlock {
|
||||
let mut props = props;
|
||||
props.delay = if props.delay == 4 { 1 } else { props.delay + 1 };
|
||||
let state = props.to_state_id(block);
|
||||
world.set_block_state(&block_pos, state, BlockFlags::empty());
|
||||
world.set_block_state(&block_pos, state, BlockFlags::NOTIFY_ALL);
|
||||
}
|
||||
|
||||
fn is_locked(
|
||||
|
||||
@ -1,29 +1,41 @@
|
||||
use pumpkin_data::Block;
|
||||
use pumpkin_data::BlockStateId;
|
||||
use pumpkin_data::block_properties::{BlockProperties, TntLikeProperties};
|
||||
use pumpkin_data::entity::EntityType;
|
||||
use pumpkin_data::item::Item;
|
||||
use pumpkin_data::sound::SoundCategory;
|
||||
use pumpkin_data::translation;
|
||||
use pumpkin_macros::pumpkin_block;
|
||||
use pumpkin_util::GameMode;
|
||||
use pumpkin_util::math::position::BlockPos;
|
||||
use pumpkin_util::math::vector3::Vector3;
|
||||
use pumpkin_util::text::TextComponent;
|
||||
use pumpkin_world::world::BlockFlags;
|
||||
use rand::RngExt;
|
||||
use std::sync::Arc;
|
||||
|
||||
use super::redstone::block_receives_redstone_power;
|
||||
use crate::block::registry::BlockActionResult;
|
||||
use crate::block::{
|
||||
BlockBehaviour, ExplodeArgs, OnNeighborUpdateArgs, PlacedArgs, UseWithItemArgs,
|
||||
BlockBehaviour, BrokenArgs, ExplodeArgs, OnNeighborUpdateArgs, OnProjectileHitArgs, PlacedArgs,
|
||||
UseWithItemArgs,
|
||||
};
|
||||
use crate::entity::Entity;
|
||||
use crate::entity::tnt::TNTEntity;
|
||||
use crate::world::World;
|
||||
use pumpkin_data::BlockStateId;
|
||||
use pumpkin_data::entity::EntityType;
|
||||
use pumpkin_data::sound::SoundCategory;
|
||||
use pumpkin_macros::pumpkin_block;
|
||||
use pumpkin_util::math::position::BlockPos;
|
||||
use pumpkin_util::math::vector3::Vector3;
|
||||
use pumpkin_world::world::BlockFlags;
|
||||
use rand::RngExt;
|
||||
|
||||
use super::redstone::block_receives_redstone_power;
|
||||
|
||||
#[pumpkin_block("minecraft:tnt")]
|
||||
pub struct TNTBlock;
|
||||
|
||||
const DEFAULT_FUSE: u32 = 80;
|
||||
const DEFAULT_POWER: f32 = 4.0;
|
||||
|
||||
impl TNTBlock {
|
||||
pub fn prime(world: &Arc<World>, location: &BlockPos) {
|
||||
pub fn prime(world: &Arc<World>, location: &BlockPos) -> bool {
|
||||
if !world.level_info.load().game_rules.tnt_explodes {
|
||||
return false;
|
||||
}
|
||||
|
||||
let mut event = crate::plugin::api::events::block::tnt_prime::TNTPrimeEvent::new(
|
||||
*location,
|
||||
"REDSTONE".to_string(),
|
||||
@ -32,10 +44,15 @@ impl TNTBlock {
|
||||
server.plugin_manager.fire_blocking(&server, &mut event);
|
||||
}
|
||||
if event.cancelled {
|
||||
return;
|
||||
return false;
|
||||
}
|
||||
|
||||
let entity = Entity::new(world.clone(), location.to_f64(), &EntityType::TNT);
|
||||
let spawn_pos = Vector3::new(
|
||||
location.0.x as f64 + 0.5,
|
||||
location.0.y as f64,
|
||||
location.0.z as f64 + 0.5,
|
||||
);
|
||||
let entity = Entity::new(world.clone(), spawn_pos, &EntityType::TNT);
|
||||
let mut prime_event =
|
||||
crate::plugin::api::events::entity::explosion_prime::ExplosionPrimeEvent::new(
|
||||
entity.entity_id,
|
||||
@ -48,65 +65,92 @@ impl TNTBlock {
|
||||
.fire_blocking(&server, &mut prime_event);
|
||||
}
|
||||
if prime_event.cancelled {
|
||||
return;
|
||||
return false;
|
||||
}
|
||||
|
||||
let pos = entity.pos.load();
|
||||
let tnt = Arc::new(TNTEntity::new(entity, DEFAULT_POWER, DEFAULT_FUSE));
|
||||
world.spawn_entity(tnt);
|
||||
world.play_sound(
|
||||
pumpkin_data::sound::Sound::EntityTntPrimed,
|
||||
SoundCategory::Blocks,
|
||||
&pos,
|
||||
&spawn_pos,
|
||||
);
|
||||
world.set_block_state(location, BlockStateId::AIR, BlockFlags::NOTIFY_ALL);
|
||||
true
|
||||
}
|
||||
}
|
||||
|
||||
const DEFAULT_FUSE: u32 = 80;
|
||||
const DEFAULT_POWER: f32 = 4.0;
|
||||
|
||||
impl BlockBehaviour for TNTBlock {
|
||||
fn use_with_item(&self, args: UseWithItemArgs<'_>) -> BlockActionResult {
|
||||
{
|
||||
let item = args.item_stack.item;
|
||||
if item != &Item::FLINT_AND_STEEL || item == &Item::FIRE_CHARGE {
|
||||
return BlockActionResult::Pass;
|
||||
}
|
||||
let world = args.player.world();
|
||||
Self::prime(&world, args.position);
|
||||
let item_id = args.item_stack.item.id;
|
||||
if item_id != Item::FLINT_AND_STEEL.id && item_id != Item::FIRE_CHARGE.id {
|
||||
return BlockActionResult::PassToDefaultBlockAction;
|
||||
}
|
||||
|
||||
BlockActionResult::Consume
|
||||
if Self::prime(args.world, args.position) {
|
||||
if args.player.gamemode.load() != GameMode::Creative {
|
||||
if item_id == Item::FLINT_AND_STEEL.id {
|
||||
let _ = args.item_stack.damage_item(1);
|
||||
} else {
|
||||
args.item_stack.decrement(1);
|
||||
}
|
||||
}
|
||||
BlockActionResult::Success
|
||||
} else if !args.world.level_info.load().game_rules.tnt_explodes {
|
||||
args.player.send_system_message_raw(
|
||||
&TextComponent::translate(translation::java::BLOCK_MINECRAFT_TNT_DISABLED, []),
|
||||
true,
|
||||
);
|
||||
BlockActionResult::Pass
|
||||
} else {
|
||||
BlockActionResult::Success
|
||||
}
|
||||
}
|
||||
|
||||
fn placed(&self, args: PlacedArgs<'_>) {
|
||||
if args.block != Block::from_state_id(args.old_state_id)
|
||||
&& block_receives_redstone_power(args.world, args.position)
|
||||
{
|
||||
if block_receives_redstone_power(args.world, args.position) {
|
||||
Self::prime(args.world, args.position);
|
||||
}
|
||||
Self::prime(args.world, args.position);
|
||||
}
|
||||
}
|
||||
|
||||
fn on_neighbor_update(&self, args: OnNeighborUpdateArgs<'_>) {
|
||||
{
|
||||
if block_receives_redstone_power(args.world, args.position) {
|
||||
if block_receives_redstone_power(args.world, args.position) {
|
||||
Self::prime(args.world, args.position);
|
||||
}
|
||||
}
|
||||
|
||||
fn broken(&self, args: BrokenArgs<'_>) {
|
||||
if args.player.gamemode.load() != GameMode::Creative {
|
||||
let props = TntLikeProperties::from_state_id(args.state.id, args.block);
|
||||
if props.r#unstable {
|
||||
Self::prime(args.world, args.position);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn explode(&self, args: ExplodeArgs<'_>) {
|
||||
{
|
||||
let entity = Entity::new(args.world.clone(), args.position.to_f64(), &EntityType::TNT);
|
||||
let angle = rand::random::<f64>() * std::f64::consts::TAU;
|
||||
entity.set_velocity(Vector3::new(-angle.sin() * 0.02, 0.2, -angle.cos() * 0.02));
|
||||
let fuse = rand::rng().random_range(0..DEFAULT_FUSE / 4) + DEFAULT_FUSE / 8;
|
||||
let tnt = Arc::new(TNTEntity::new(entity, DEFAULT_POWER, fuse));
|
||||
args.world.spawn_entity(tnt);
|
||||
fn on_projectile_hit(&self, args: OnProjectileHitArgs<'_>) {
|
||||
if args.projectile.get_entity().is_on_fire() {
|
||||
Self::prime(args.world, args.position);
|
||||
}
|
||||
}
|
||||
|
||||
fn explode(&self, args: ExplodeArgs<'_>) {
|
||||
if !args.world.level_info.load().game_rules.tnt_explodes {
|
||||
return;
|
||||
}
|
||||
let spawn_pos = Vector3::new(
|
||||
args.position.0.x as f64 + 0.5,
|
||||
args.position.0.y as f64,
|
||||
args.position.0.z as f64 + 0.5,
|
||||
);
|
||||
let entity = Entity::new(args.world.clone(), spawn_pos, &EntityType::TNT);
|
||||
let fuse = rand::rng().random_range(0..DEFAULT_FUSE / 4) + DEFAULT_FUSE / 8;
|
||||
let tnt = Arc::new(TNTEntity::new(entity, DEFAULT_POWER, fuse));
|
||||
args.world.spawn_entity(tnt);
|
||||
}
|
||||
|
||||
fn should_drop_items_on_explosion(&self) -> bool {
|
||||
false
|
||||
}
|
||||
|
||||
@ -82,6 +82,8 @@ pub trait BlockBehaviour: Send + Sync {
|
||||
|
||||
fn on_entity_collision(&self, _args: OnEntityCollisionArgs<'_>) {}
|
||||
|
||||
fn on_projectile_hit(&self, _args: OnProjectileHitArgs<'_>) {}
|
||||
|
||||
/// Called when an entity is standing on / walking over the top face of this block.
|
||||
fn on_entity_step(&self, _args: OnEntityStepArgs<'_>) {}
|
||||
|
||||
@ -227,6 +229,16 @@ pub struct OnEntityCollisionArgs<'a> {
|
||||
pub entity: &'a dyn EntityBase,
|
||||
}
|
||||
|
||||
pub struct OnProjectileHitArgs<'a> {
|
||||
pub server: &'a Server,
|
||||
pub world: &'a Arc<World>,
|
||||
pub block: &'a Block,
|
||||
pub state: &'a BlockState,
|
||||
pub position: &'a BlockPos,
|
||||
pub projectile: &'a dyn EntityBase,
|
||||
pub hit_pos: &'a Vector3<f64>,
|
||||
}
|
||||
|
||||
pub struct OnEntityStepArgs<'a> {
|
||||
pub world: &'a Arc<World>,
|
||||
pub block: &'a Block,
|
||||
|
||||
@ -159,8 +159,8 @@ use crate::block::fluid::lava::FlowingLava;
|
||||
use crate::block::fluid::water::FlowingWater;
|
||||
use crate::block::{
|
||||
BlockBehaviour, BlockHitResult, BlockMetadata, BonemealArgs, FluidMetadata,
|
||||
GetInsideCollisionShapeArgs, OnEntityCollisionArgs, OnLandedUponArgs,
|
||||
UpdateEntityMovementAfterFallOnArgs, stop_vertical_movement_after_fall,
|
||||
GetInsideCollisionShapeArgs, OnEntityCollisionArgs, OnEntityStepArgs, OnLandedUponArgs,
|
||||
OnProjectileHitArgs, UpdateEntityMovementAfterFallOnArgs, stop_vertical_movement_after_fall,
|
||||
};
|
||||
use crate::entity::EntityBase;
|
||||
use crate::entity::player::Player;
|
||||
@ -176,6 +176,7 @@ use pumpkin_data::{Block, BlockDirection, BlockId, BlockState};
|
||||
use pumpkin_protocol::java::server::play::SUseItemOn;
|
||||
use pumpkin_util::math::boundingbox::BoundingBox;
|
||||
use pumpkin_util::math::position::BlockPos;
|
||||
use pumpkin_util::math::vector3::Vector3;
|
||||
use pumpkin_world::world::{BlockAccessor, BlockFlags};
|
||||
use rustc_hash::FxHashMap;
|
||||
use std::sync::Arc;
|
||||
@ -190,7 +191,6 @@ use super::{
|
||||
OnPlaceArgs, OnStateReplacedArgs, OnSyncedBlockEventArgs, PlacedArgs, PlayerPlacedArgs,
|
||||
PrepareArgs, UseWithItemArgs,
|
||||
};
|
||||
use crate::block::OnEntityStepArgs;
|
||||
use crate::block::blocks::blast_furnace::BlastFurnaceBlock;
|
||||
use crate::block::blocks::chain::ChainBlock;
|
||||
use crate::block::blocks::cobweb::CobwebBlock;
|
||||
@ -818,6 +818,30 @@ impl BlockRegistry {
|
||||
}
|
||||
}
|
||||
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
pub fn on_projectile_hit(
|
||||
&self,
|
||||
block: &Block,
|
||||
world: &Arc<World>,
|
||||
projectile: &dyn EntityBase,
|
||||
position: &BlockPos,
|
||||
state: &BlockState,
|
||||
hit_pos: &Vector3<f64>,
|
||||
server: &Server,
|
||||
) {
|
||||
if let Some(pumpkin_block) = self.get_pumpkin_block(block.id) {
|
||||
pumpkin_block.on_projectile_hit(OnProjectileHitArgs {
|
||||
server,
|
||||
world,
|
||||
block,
|
||||
state,
|
||||
position,
|
||||
projectile,
|
||||
hit_pos,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
pub fn on_entity_collision_fluid(&self, fluid: &Fluid, entity: &dyn EntityBase) {
|
||||
let pumpkin_fluid = self.get_pumpkin_fluid(fluid.id);
|
||||
if let Some(pumpkin_fluid) = pumpkin_fluid {
|
||||
|
||||
@ -238,6 +238,8 @@ impl CommandExecutor for CloneExecutor {
|
||||
}
|
||||
}
|
||||
|
||||
world.flush_block_updates();
|
||||
|
||||
context.source.send_feedback(
|
||||
TextComponent::translate_cross(
|
||||
translation::java::COMMANDS_CLONE_SUCCESS,
|
||||
|
||||
@ -2,6 +2,7 @@ use pumpkin_data::translation;
|
||||
use pumpkin_data::{Block, BlockStateId};
|
||||
use pumpkin_util::PermissionLvl;
|
||||
use pumpkin_util::math::position::BlockPos;
|
||||
use pumpkin_util::math::vector2::Vector2;
|
||||
use pumpkin_util::math::vector3::Vector3;
|
||||
use pumpkin_util::permission::{Permission, PermissionDefault, PermissionRegistry};
|
||||
use pumpkin_util::text::TextComponent;
|
||||
@ -49,6 +50,7 @@ enum FilterMode {
|
||||
KeepAir,
|
||||
}
|
||||
|
||||
#[expect(clippy::too_many_lines)]
|
||||
fn fill_blocks(
|
||||
source: &CommandSource,
|
||||
from: BlockPos,
|
||||
@ -84,68 +86,146 @@ fn fill_blocks(
|
||||
}
|
||||
|
||||
let target_state_id = target_block.default_state.id;
|
||||
let mut count = 0;
|
||||
let mut changed_positions = Vec::new();
|
||||
|
||||
for x in min_x..=max_x {
|
||||
for y in min_y..=max_y {
|
||||
for z in min_z..=max_z {
|
||||
let pos = BlockPos(Vector3::new(x, y, z));
|
||||
let current_block = world.get_block(&pos);
|
||||
let min_chunk_x = min_x >> 4;
|
||||
let max_chunk_x = max_x >> 4;
|
||||
let min_chunk_z = min_z >> 4;
|
||||
let max_chunk_z = max_z >> 4;
|
||||
|
||||
match mode {
|
||||
FillMode::Keep => {
|
||||
if !current_block.is_air() {
|
||||
for chunk_x in min_chunk_x..=max_chunk_x {
|
||||
for chunk_z in min_chunk_z..=max_chunk_z {
|
||||
let chunk_pos = Vector2::new(chunk_x, chunk_z);
|
||||
let local_start_x = (min_x - (chunk_x << 4)).clamp(0, 15) as usize;
|
||||
let local_end_x = (max_x - (chunk_x << 4)).clamp(0, 15) as usize;
|
||||
let local_start_z = (min_z - (chunk_z << 4)).clamp(0, 15) as usize;
|
||||
let local_end_z = (max_z - (chunk_z << 4)).clamp(0, 15) as usize;
|
||||
|
||||
let result = world.level.read_chunk_sync(&chunk_pos, |chunk| {
|
||||
let mut updates_for_chunk = Vec::new();
|
||||
let mut block_entities_to_remove = Vec::new();
|
||||
let min_y_chunk = chunk.section.min_y;
|
||||
|
||||
{
|
||||
let block_sections = chunk
|
||||
.section
|
||||
.block_sections
|
||||
.read()
|
||||
.unwrap_or_else(std::sync::PoisonError::into_inner);
|
||||
|
||||
for y in min_y..=max_y {
|
||||
let y_rel = y - min_y_chunk;
|
||||
if y_rel < 0 {
|
||||
continue;
|
||||
}
|
||||
}
|
||||
_ => {
|
||||
if let Some(f) = filter
|
||||
&& !f.test(current_block)
|
||||
{
|
||||
let rel_y = y_rel as usize;
|
||||
let section_index = rel_y / pumpkin_world::chunk::CHUNK_WIDTH;
|
||||
let sub_y = rel_y % pumpkin_world::chunk::CHUNK_WIDTH;
|
||||
|
||||
let Some(section) = block_sections.get(section_index) else {
|
||||
continue;
|
||||
};
|
||||
|
||||
for rel_z in local_start_z..=local_end_z {
|
||||
for rel_x in local_start_x..=local_end_x {
|
||||
let global_x = (chunk_x << 4) + rel_x as i32;
|
||||
let global_z = (chunk_z << 4) + rel_z as i32;
|
||||
let pos = BlockPos(Vector3::new(global_x, y, global_z));
|
||||
|
||||
let current_state_id = section.get(rel_x, sub_y, rel_z);
|
||||
let current_block = Block::from_state_id(current_state_id);
|
||||
|
||||
match mode {
|
||||
FillMode::Keep => {
|
||||
if !current_block.is_air() {
|
||||
continue;
|
||||
}
|
||||
}
|
||||
_ => {
|
||||
if let Some(f) = filter
|
||||
&& !f.test(current_block)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let is_edge = global_x == min_x
|
||||
|| global_x == max_x
|
||||
|| y == min_y
|
||||
|| y == max_y
|
||||
|| global_z == min_z
|
||||
|| global_z == max_z;
|
||||
|
||||
let block_to_place = match mode {
|
||||
FillMode::Outline => is_edge.then_some(target_state_id),
|
||||
FillMode::Hollow => {
|
||||
if is_edge {
|
||||
Some(target_state_id)
|
||||
} else {
|
||||
Some(BlockStateId::AIR)
|
||||
}
|
||||
}
|
||||
FillMode::Destroy => {
|
||||
if current_state_id != target_state_id {
|
||||
world.break_block(
|
||||
&pos,
|
||||
None,
|
||||
BlockFlags::SKIP_DROPS
|
||||
| BlockFlags::NOTIFY_ALL
|
||||
| BlockFlags::FORCE_STATE,
|
||||
);
|
||||
}
|
||||
Some(target_state_id)
|
||||
}
|
||||
FillMode::Replace | FillMode::Keep => Some(target_state_id),
|
||||
};
|
||||
|
||||
if let Some(state_id) = block_to_place {
|
||||
if current_block.default_state.block_entity_type != u16::MAX {
|
||||
block_entities_to_remove.push(pos);
|
||||
}
|
||||
updates_for_chunk.push((rel_x, y, rel_z, state_id, pos));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let is_edge = x == min_x
|
||||
|| x == max_x
|
||||
|| y == min_y
|
||||
|| y == max_y
|
||||
|| z == min_z
|
||||
|| z == max_z;
|
||||
|
||||
let block_to_place = match mode {
|
||||
FillMode::Outline => is_edge.then_some(target_state_id),
|
||||
FillMode::Hollow => {
|
||||
if is_edge {
|
||||
Some(target_state_id)
|
||||
} else {
|
||||
Some(BlockStateId::AIR)
|
||||
}
|
||||
}
|
||||
FillMode::Destroy => {
|
||||
world.break_block(
|
||||
&pos,
|
||||
None,
|
||||
BlockFlags::SKIP_DROPS | BlockFlags::FORCE_STATE,
|
||||
);
|
||||
Some(target_state_id)
|
||||
}
|
||||
FillMode::Replace | FillMode::Keep => Some(target_state_id),
|
||||
};
|
||||
|
||||
if let Some(state_id) = block_to_place {
|
||||
world.set_block_state(&pos, state_id, BlockFlags::FORCE_STATE);
|
||||
count += 1;
|
||||
if updates_for_chunk.is_empty() {
|
||||
return (Vec::new(), block_entities_to_remove);
|
||||
}
|
||||
|
||||
let batch_inputs = updates_for_chunk
|
||||
.iter()
|
||||
.map(|&(rx, y, rz, sid, _)| (rx, y, rz, sid));
|
||||
chunk.set_blocks_batch(batch_inputs);
|
||||
|
||||
let chunk_changed = updates_for_chunk
|
||||
.into_iter()
|
||||
.map(|(_, _, _, new_id, pos)| (pos, new_id))
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
(chunk_changed, block_entities_to_remove)
|
||||
});
|
||||
|
||||
if let Some((chunk_changed, be_to_remove)) = result {
|
||||
for pos in be_to_remove {
|
||||
world.remove_block_entity(&pos);
|
||||
}
|
||||
changed_positions.extend(chunk_changed);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let count = changed_positions.len() as i32;
|
||||
if count == 0 {
|
||||
return Err(ERROR_FAILED.create_without_context());
|
||||
}
|
||||
|
||||
world.queue_block_updates(&changed_positions);
|
||||
world.flush_block_updates();
|
||||
|
||||
source.send_feedback(
|
||||
TextComponent::translate_cross(
|
||||
translation::java::COMMANDS_FILL_SUCCESS,
|
||||
|
||||
@ -47,11 +47,15 @@ impl CommandExecutor for SetBlockExecutor {
|
||||
|
||||
let success = match mode {
|
||||
Mode::Destroy => {
|
||||
world.break_block(&pos, None, BlockFlags::SKIP_DROPS | BlockFlags::FORCE_STATE);
|
||||
world.break_block(
|
||||
&pos,
|
||||
None,
|
||||
BlockFlags::SKIP_DROPS | BlockFlags::NOTIFY_ALL | BlockFlags::FORCE_STATE,
|
||||
);
|
||||
world.set_block_state(
|
||||
&pos,
|
||||
block_state_id,
|
||||
BlockFlags::FORCE_STATE | BlockFlags::NOTIFY_NEIGHBORS,
|
||||
BlockFlags::NOTIFY_ALL | BlockFlags::FORCE_STATE,
|
||||
);
|
||||
true
|
||||
}
|
||||
@ -59,7 +63,7 @@ impl CommandExecutor for SetBlockExecutor {
|
||||
world.set_block_state(
|
||||
&pos,
|
||||
block_state_id,
|
||||
BlockFlags::FORCE_STATE | BlockFlags::NOTIFY_NEIGHBORS,
|
||||
BlockFlags::NOTIFY_ALL | BlockFlags::FORCE_STATE,
|
||||
);
|
||||
true
|
||||
}
|
||||
@ -69,7 +73,7 @@ impl CommandExecutor for SetBlockExecutor {
|
||||
world.set_block_state(
|
||||
&pos,
|
||||
block_state_id,
|
||||
BlockFlags::FORCE_STATE | BlockFlags::NOTIFY_NEIGHBORS,
|
||||
BlockFlags::NOTIFY_ALL | BlockFlags::FORCE_STATE,
|
||||
);
|
||||
true
|
||||
} else {
|
||||
@ -77,12 +81,19 @@ impl CommandExecutor for SetBlockExecutor {
|
||||
}
|
||||
}
|
||||
Mode::Strict => {
|
||||
world.set_block_state(&pos, block_state_id, BlockFlags::SKIP_BLOCK_ADDED_CALLBACK);
|
||||
world.set_block_state(
|
||||
&pos,
|
||||
block_state_id,
|
||||
BlockFlags::NOTIFY_LISTENERS
|
||||
| BlockFlags::SKIP_BLOCK_ADDED_CALLBACK
|
||||
| BlockFlags::FORCE_STATE,
|
||||
);
|
||||
true
|
||||
}
|
||||
};
|
||||
|
||||
if success {
|
||||
world.flush_block_updates();
|
||||
context.source.send_feedback(
|
||||
TextComponent::translate_cross(
|
||||
pumpkin_data::translation::java::COMMANDS_SETBLOCK_SUCCESS,
|
||||
|
||||
19
crates/pumpkin/src/enchantment/effects/add_value.rs
Normal file
19
crates/pumpkin/src/enchantment/effects/add_value.rs
Normal file
@ -0,0 +1,19 @@
|
||||
use pumpkin_data::enchantment::LevelBasedValue;
|
||||
|
||||
/// Matches vanilla `net.minecraft.world.item.enchantment.effects.AddValue`.
|
||||
#[derive(Clone, Debug, PartialEq)]
|
||||
pub struct AddValue {
|
||||
pub value: LevelBasedValue,
|
||||
}
|
||||
|
||||
impl AddValue {
|
||||
#[must_use]
|
||||
pub const fn new(value: LevelBasedValue) -> Self {
|
||||
Self { value }
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub fn process(&self, level: i32, current_value: f32) -> f32 {
|
||||
current_value + self.value.calculate(level)
|
||||
}
|
||||
}
|
||||
14
crates/pumpkin/src/enchantment/effects/all_of.rs
Normal file
14
crates/pumpkin/src/enchantment/effects/all_of.rs
Normal file
@ -0,0 +1,14 @@
|
||||
use pumpkin_data::enchantment::EnchantmentEntityEffect;
|
||||
|
||||
/// Matches vanilla `net.minecraft.world.item.enchantment.effects.AllOf`.
|
||||
#[derive(Clone, Debug, PartialEq)]
|
||||
pub struct AllOf<'a> {
|
||||
pub effects: &'a [EnchantmentEntityEffect],
|
||||
}
|
||||
|
||||
impl<'a> AllOf<'a> {
|
||||
#[must_use]
|
||||
pub const fn new(effects: &'a [EnchantmentEntityEffect]) -> Self {
|
||||
Self { effects }
|
||||
}
|
||||
}
|
||||
19
crates/pumpkin/src/enchantment/effects/damage_entity.rs
Normal file
19
crates/pumpkin/src/enchantment/effects/damage_entity.rs
Normal file
@ -0,0 +1,19 @@
|
||||
use pumpkin_data::enchantment::LevelBasedValue;
|
||||
|
||||
/// Matches vanilla `net.minecraft.world.item.enchantment.effects.DamageEntity`.
|
||||
#[derive(Clone, Debug, PartialEq)]
|
||||
pub struct DamageEntity {
|
||||
pub damage: LevelBasedValue,
|
||||
}
|
||||
|
||||
impl DamageEntity {
|
||||
#[must_use]
|
||||
pub const fn new(damage: LevelBasedValue) -> Self {
|
||||
Self { damage }
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub fn calculate_damage(&self, level: i32) -> f32 {
|
||||
self.damage.calculate(level)
|
||||
}
|
||||
}
|
||||
68
crates/pumpkin/src/enchantment/effects/explode.rs
Normal file
68
crates/pumpkin/src/enchantment/effects/explode.rs
Normal file
@ -0,0 +1,68 @@
|
||||
use pumpkin_data::damage::DamageType;
|
||||
use pumpkin_data::enchantment::LevelBasedValue;
|
||||
use pumpkin_data::particle::Particle;
|
||||
use pumpkin_data::sound::Sound;
|
||||
use pumpkin_util::math::vector3::Vector3;
|
||||
|
||||
use crate::world::ExplosionInteraction;
|
||||
|
||||
/// Enchantment entity effect that produces an explosion at an offset position.
|
||||
/// Matches vanilla `net.minecraft.world.item.enchantment.effects.ExplodeEffect`.
|
||||
#[derive(Clone, Debug, PartialEq)]
|
||||
pub struct ExplodeEffect {
|
||||
pub attribute_to_user: bool,
|
||||
pub damage_type: Option<&'static DamageType>,
|
||||
pub knockback_multiplier: Option<LevelBasedValue>,
|
||||
pub immune_blocks: Option<&'static str>,
|
||||
pub offset: Vector3<f64>,
|
||||
pub radius: LevelBasedValue,
|
||||
pub create_fire: bool,
|
||||
pub block_interaction: ExplosionInteraction,
|
||||
pub small_particle: Option<Particle>,
|
||||
pub large_particle: Option<Particle>,
|
||||
pub sound: Option<Sound>,
|
||||
}
|
||||
|
||||
impl ExplodeEffect {
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
#[must_use]
|
||||
pub const fn new(
|
||||
attribute_to_user: bool,
|
||||
damage_type: Option<&'static DamageType>,
|
||||
knockback_multiplier: Option<LevelBasedValue>,
|
||||
immune_blocks: Option<&'static str>,
|
||||
offset: Vector3<f64>,
|
||||
radius: LevelBasedValue,
|
||||
create_fire: bool,
|
||||
block_interaction: ExplosionInteraction,
|
||||
small_particle: Option<Particle>,
|
||||
large_particle: Option<Particle>,
|
||||
sound: Option<Sound>,
|
||||
) -> Self {
|
||||
Self {
|
||||
attribute_to_user,
|
||||
damage_type,
|
||||
knockback_multiplier,
|
||||
immune_blocks,
|
||||
offset,
|
||||
radius,
|
||||
create_fire,
|
||||
block_interaction,
|
||||
small_particle,
|
||||
large_particle,
|
||||
sound,
|
||||
}
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub fn calculate_radius(&self, level: i32) -> f32 {
|
||||
self.radius.calculate(level).max(0.0)
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub fn calculate_knockback(&self, level: i32) -> Option<f32> {
|
||||
self.knockback_multiplier
|
||||
.as_ref()
|
||||
.map(|kb| kb.calculate(level))
|
||||
}
|
||||
}
|
||||
23
crates/pumpkin/src/enchantment/effects/ignite.rs
Normal file
23
crates/pumpkin/src/enchantment/effects/ignite.rs
Normal file
@ -0,0 +1,23 @@
|
||||
use crate::entity::{Entity, EntityBase};
|
||||
use pumpkin_data::enchantment::LevelBasedValue;
|
||||
|
||||
/// Enchantment entity effect that sets an entity on fire for a duration calculated from the level.
|
||||
/// Matches vanilla `net.minecraft.world.item.enchantment.effects.Ignite`.
|
||||
#[derive(Clone, Debug, PartialEq)]
|
||||
pub struct Ignite {
|
||||
pub duration: LevelBasedValue,
|
||||
}
|
||||
|
||||
impl Ignite {
|
||||
#[must_use]
|
||||
pub const fn new(duration: LevelBasedValue) -> Self {
|
||||
Self { duration }
|
||||
}
|
||||
|
||||
/// Applies the ignite effect to an entity for the given enchantment level.
|
||||
pub fn apply(&self, level: i32, entity: &Entity) {
|
||||
let seconds = self.duration.calculate(level);
|
||||
entity.set_on_fire_for(seconds);
|
||||
entity.set_on_fire(true);
|
||||
}
|
||||
}
|
||||
21
crates/pumpkin/src/enchantment/effects/mod.rs
Normal file
21
crates/pumpkin/src/enchantment/effects/mod.rs
Normal file
@ -0,0 +1,21 @@
|
||||
pub mod add_value;
|
||||
pub mod all_of;
|
||||
pub mod damage_entity;
|
||||
pub mod explode;
|
||||
pub mod ignite;
|
||||
pub mod multiply_value;
|
||||
pub mod play_sound;
|
||||
pub mod remove_binomial;
|
||||
pub mod replace_block;
|
||||
pub mod set_value;
|
||||
|
||||
pub use add_value::AddValue;
|
||||
pub use all_of::AllOf;
|
||||
pub use damage_entity::DamageEntity;
|
||||
pub use explode::ExplodeEffect;
|
||||
pub use ignite::Ignite;
|
||||
pub use multiply_value::MultiplyValue;
|
||||
pub use play_sound::PlaySound;
|
||||
pub use remove_binomial::RemoveBinomial;
|
||||
pub use replace_block::ReplaceBlock;
|
||||
pub use set_value::SetValue;
|
||||
19
crates/pumpkin/src/enchantment/effects/multiply_value.rs
Normal file
19
crates/pumpkin/src/enchantment/effects/multiply_value.rs
Normal file
@ -0,0 +1,19 @@
|
||||
use pumpkin_data::enchantment::LevelBasedValue;
|
||||
|
||||
/// Matches vanilla `net.minecraft.world.item.enchantment.effects.MultiplyValue`.
|
||||
#[derive(Clone, Debug, PartialEq)]
|
||||
pub struct MultiplyValue {
|
||||
pub factor: LevelBasedValue,
|
||||
}
|
||||
|
||||
impl MultiplyValue {
|
||||
#[must_use]
|
||||
pub const fn new(factor: LevelBasedValue) -> Self {
|
||||
Self { factor }
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub fn process(&self, level: i32, current_value: f32) -> f32 {
|
||||
current_value * self.factor.calculate(level)
|
||||
}
|
||||
}
|
||||
12
crates/pumpkin/src/enchantment/effects/play_sound.rs
Normal file
12
crates/pumpkin/src/enchantment/effects/play_sound.rs
Normal file
@ -0,0 +1,12 @@
|
||||
/// Matches vanilla `net.minecraft.world.item.enchantment.effects.PlaySound`.
|
||||
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||
pub struct PlaySound {
|
||||
pub sound: &'static str,
|
||||
}
|
||||
|
||||
impl PlaySound {
|
||||
#[must_use]
|
||||
pub const fn new(sound: &'static str) -> Self {
|
||||
Self { sound }
|
||||
}
|
||||
}
|
||||
24
crates/pumpkin/src/enchantment/effects/remove_binomial.rs
Normal file
24
crates/pumpkin/src/enchantment/effects/remove_binomial.rs
Normal file
@ -0,0 +1,24 @@
|
||||
use pumpkin_data::enchantment::LevelBasedValue;
|
||||
|
||||
/// Matches vanilla `net.minecraft.world.item.enchantment.effects.RemoveBinomial`.
|
||||
#[derive(Clone, Debug, PartialEq)]
|
||||
pub struct RemoveBinomial {
|
||||
pub chance: LevelBasedValue,
|
||||
}
|
||||
|
||||
impl RemoveBinomial {
|
||||
#[must_use]
|
||||
pub const fn new(chance: LevelBasedValue) -> Self {
|
||||
Self { chance }
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub fn process(&self, level: i32, current_value: f32) -> f32 {
|
||||
let prob = self.chance.calculate(level);
|
||||
if rand::random::<f32>() < prob {
|
||||
0.0
|
||||
} else {
|
||||
current_value
|
||||
}
|
||||
}
|
||||
}
|
||||
33
crates/pumpkin/src/enchantment/effects/replace_block.rs
Normal file
33
crates/pumpkin/src/enchantment/effects/replace_block.rs
Normal file
@ -0,0 +1,33 @@
|
||||
use pumpkin_data::game_event::GameEvent;
|
||||
use pumpkin_util::math::position::BlockPos;
|
||||
use pumpkin_util::math::vector3::Vector3;
|
||||
|
||||
/// Enchantment entity effect that replaces a block at an offset position.
|
||||
/// Matches vanilla `net.minecraft.world.item.enchantment.effects.ReplaceBlock`.
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
|
||||
pub struct ReplaceBlock {
|
||||
pub offset: Vector3<i32>,
|
||||
pub trigger_game_event: Option<GameEvent>,
|
||||
}
|
||||
|
||||
impl ReplaceBlock {
|
||||
#[must_use]
|
||||
pub const fn new(offset: Vector3<i32>, trigger_game_event: Option<GameEvent>) -> Self {
|
||||
Self {
|
||||
offset,
|
||||
trigger_game_event,
|
||||
}
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub const fn target_position(&self, origin: Vector3<f64>) -> BlockPos {
|
||||
let base_x = origin.x.floor() as i32;
|
||||
let base_y = origin.y.floor() as i32;
|
||||
let base_z = origin.z.floor() as i32;
|
||||
BlockPos::new(
|
||||
base_x + self.offset.x,
|
||||
base_y + self.offset.y,
|
||||
base_z + self.offset.z,
|
||||
)
|
||||
}
|
||||
}
|
||||
19
crates/pumpkin/src/enchantment/effects/set_value.rs
Normal file
19
crates/pumpkin/src/enchantment/effects/set_value.rs
Normal file
@ -0,0 +1,19 @@
|
||||
use pumpkin_data::enchantment::LevelBasedValue;
|
||||
|
||||
/// Matches vanilla `net.minecraft.world.item.enchantment.effects.SetValue`.
|
||||
#[derive(Clone, Debug, PartialEq)]
|
||||
pub struct SetValue {
|
||||
pub value: LevelBasedValue,
|
||||
}
|
||||
|
||||
impl SetValue {
|
||||
#[must_use]
|
||||
pub const fn new(value: LevelBasedValue) -> Self {
|
||||
Self { value }
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub fn process(&self, level: i32) -> f32 {
|
||||
self.value.calculate(level)
|
||||
}
|
||||
}
|
||||
169
crates/pumpkin/src/enchantment/helper.rs
Normal file
169
crates/pumpkin/src/enchantment/helper.rs
Normal file
@ -0,0 +1,169 @@
|
||||
use crate::enchantment::effects::Ignite;
|
||||
use crate::entity::Entity;
|
||||
use crate::entity::projectile::arrow::ArrowEntity;
|
||||
use pumpkin_data::data_component_impl::EnchantmentsImpl;
|
||||
use pumpkin_data::enchantment::{Enchantment, EnchantmentEntityEffect, EnchantmentTarget};
|
||||
use pumpkin_data::item::Item;
|
||||
use pumpkin_data::item_stack::ItemStack;
|
||||
|
||||
/// Data-driven helper for enchantment effects matching vanilla `EnchantmentHelper`.
|
||||
pub struct EnchantmentHelper;
|
||||
|
||||
impl EnchantmentHelper {
|
||||
/// Iterates through enchantments on an item stack, matching vanilla `runIterationOnItem`.
|
||||
pub fn run_iteration_on_item<F>(item_stack: &ItemStack, mut visitor: F)
|
||||
where
|
||||
F: FnMut(&'static Enchantment, i32),
|
||||
{
|
||||
if let Some(enchantments) = item_stack.get_data_component::<EnchantmentsImpl>() {
|
||||
for (enchantment, level) in enchantments.enchantment.iter() {
|
||||
visitor(enchantment, *level);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Applies projectile-spawned enchantment effects (e.g. Flame ignites the projectile for 100s).
|
||||
pub fn on_projectile_spawned(
|
||||
weapon: &ItemStack,
|
||||
projectile_entity: &Entity,
|
||||
arrow: Option<&ArrowEntity>,
|
||||
) {
|
||||
Self::run_iteration_on_item(weapon, |enchantment, level| {
|
||||
for conditional_effect in enchantment.effects.projectile_spawned {
|
||||
if let EnchantmentEntityEffect::Ignite { duration } = &conditional_effect.effect {
|
||||
let ignite = Ignite::new(duration.clone());
|
||||
ignite.apply(level, projectile_entity);
|
||||
if let Some(arrow) = arrow {
|
||||
arrow.set_flame(true);
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/// Applies post-attack enchantment effects (e.g. Fire Aspect ignites victim).
|
||||
pub fn on_post_attack(_attacker: &Entity, victim: &Entity, weapon: &ItemStack) {
|
||||
Self::run_iteration_on_item(weapon, |enchantment, level| {
|
||||
for targeted_effect in enchantment.effects.post_attack {
|
||||
if targeted_effect.affected == Some(EnchantmentTarget::Victim)
|
||||
&& let EnchantmentEntityEffect::Ignite { duration } = &targeted_effect.effect
|
||||
{
|
||||
let ignite = Ignite::new(duration.clone());
|
||||
ignite.apply(level, victim);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/// Computes projectile spread angle using data-driven projectile spread effects.
|
||||
#[must_use]
|
||||
pub fn process_projectile_spread(weapon: &ItemStack, base_spread: f32) -> f32 {
|
||||
let mut spread = base_spread;
|
||||
if let Some(enchantments) = weapon.get_data_component::<EnchantmentsImpl>() {
|
||||
for (enchantment, level) in enchantments.enchantment.iter() {
|
||||
enchantment.modify_projectile_spread(*level, &mut spread);
|
||||
}
|
||||
}
|
||||
spread
|
||||
}
|
||||
|
||||
/// Computes projectile count using data-driven projectile count effects.
|
||||
#[must_use]
|
||||
pub fn process_projectile_count(weapon: &ItemStack, base_count: usize) -> usize {
|
||||
let mut count = base_count as f32;
|
||||
if let Some(enchantments) = weapon.get_data_component::<EnchantmentsImpl>() {
|
||||
for (enchantment, level) in enchantments.enchantment.iter() {
|
||||
enchantment.modify_projectile_count(*level, &mut count);
|
||||
}
|
||||
}
|
||||
count as usize
|
||||
}
|
||||
|
||||
/// Computes projectile piercing level using data-driven piercing effects.
|
||||
#[must_use]
|
||||
pub fn process_projectile_piercing(weapon: &ItemStack, base_piercing: u8) -> u8 {
|
||||
let mut piercing = f32::from(base_piercing);
|
||||
if let Some(enchantments) = weapon.get_data_component::<EnchantmentsImpl>() {
|
||||
for (enchantment, level) in enchantments.enchantment.iter() {
|
||||
enchantment.modify_piercing_count(*level, &mut piercing);
|
||||
}
|
||||
}
|
||||
piercing as u8
|
||||
}
|
||||
|
||||
/// Computes ammo use using data-driven ammo use effects (e.g. Infinity).
|
||||
#[must_use]
|
||||
pub fn process_ammo_use(weapon: &ItemStack, projectile: &ItemStack, base_ammo: i32) -> i32 {
|
||||
let mut ammo = base_ammo as f32;
|
||||
if projectile.item.id == Item::ARROW.id
|
||||
&& let Some(enchantments) = weapon.get_data_component::<EnchantmentsImpl>()
|
||||
{
|
||||
for (enchantment, level) in enchantments.enchantment.iter() {
|
||||
enchantment.modify_ammo_count(*level, &mut ammo);
|
||||
}
|
||||
}
|
||||
ammo as i32
|
||||
}
|
||||
|
||||
/// Modifies damage using data-driven damage effects (e.g. Power / Sharpness).
|
||||
#[must_use]
|
||||
pub fn modify_damage(weapon: &ItemStack, base_damage: f64) -> f64 {
|
||||
let mut damage = base_damage;
|
||||
if let Some(enchantments) = weapon.get_data_component::<EnchantmentsImpl>() {
|
||||
for (enchantment, level) in enchantments.enchantment.iter() {
|
||||
enchantment.modify_damage(*level, &mut damage);
|
||||
}
|
||||
}
|
||||
damage
|
||||
}
|
||||
|
||||
/// Modifies smash / fall-based damage using data-driven effects (e.g. Density).
|
||||
#[must_use]
|
||||
pub fn modify_fall_based_damage(weapon: &ItemStack, base_damage: f64) -> f64 {
|
||||
let mut damage = base_damage;
|
||||
if let Some(enchantments) = weapon.get_data_component::<EnchantmentsImpl>() {
|
||||
for (enchantment, level) in enchantments.enchantment.iter() {
|
||||
enchantment.modify_fall_based_damage(*level, &mut damage);
|
||||
}
|
||||
}
|
||||
damage
|
||||
}
|
||||
|
||||
/// Modifies knockback using data-driven knockback effects (e.g. Punch / Knockback).
|
||||
#[must_use]
|
||||
pub fn modify_knockback(weapon: &ItemStack, base_knockback: f32) -> f32 {
|
||||
let mut knockback = base_knockback;
|
||||
if let Some(enchantments) = weapon.get_data_component::<EnchantmentsImpl>() {
|
||||
for (enchantment, level) in enchantments.enchantment.iter() {
|
||||
enchantment.modify_knockback(*level, &mut knockback);
|
||||
}
|
||||
}
|
||||
knockback
|
||||
}
|
||||
|
||||
/// Modifies armor effectiveness using data-driven effects (e.g. Breach).
|
||||
#[must_use]
|
||||
pub fn modify_armor_effectiveness(weapon: &ItemStack, base_effectiveness: f32) -> f32 {
|
||||
let mut effectiveness = base_effectiveness;
|
||||
if let Some(enchantments) = weapon.get_data_component::<EnchantmentsImpl>() {
|
||||
for (enchantment, level) in enchantments.enchantment.iter() {
|
||||
enchantment.modify_armor_effectiveness(*level, &mut effectiveness);
|
||||
}
|
||||
}
|
||||
effectiveness
|
||||
}
|
||||
|
||||
/// Modifies crossbow charge time using data-driven charge time effects (e.g. Quick Charge).
|
||||
#[must_use]
|
||||
pub fn modify_crossbow_charge_time(weapon: &ItemStack, base_ticks: i32) -> i32 {
|
||||
let mut charge_time = base_ticks as f32;
|
||||
if let Some(enchantments) = weapon.get_data_component::<EnchantmentsImpl>() {
|
||||
for (enchantment, level) in enchantments.enchantment.iter() {
|
||||
let mut change_sec = 0.0f32;
|
||||
enchantment.modify_crossbow_charge_time(*level, &mut change_sec);
|
||||
charge_time += change_sec * 20.0;
|
||||
}
|
||||
}
|
||||
(charge_time as i32).max(0)
|
||||
}
|
||||
}
|
||||
6
crates/pumpkin/src/enchantment/mod.rs
Normal file
6
crates/pumpkin/src/enchantment/mod.rs
Normal file
@ -0,0 +1,6 @@
|
||||
pub mod effects;
|
||||
pub mod helper;
|
||||
|
||||
pub use effects::*;
|
||||
pub use helper::EnchantmentHelper;
|
||||
pub use pumpkin_data::enchantment::*;
|
||||
@ -75,7 +75,31 @@ impl BowAttackGoal {
|
||||
|
||||
let arrow_entity = Entity::new(world.clone(), entity.pos.load(), &EntityType::ARROW);
|
||||
let projectile = ItemStack::new(1, &Item::ARROW);
|
||||
let arrow = ArrowEntity::new_shot(arrow_entity, entity, &projectile, ArrowPickup::Allowed);
|
||||
let bow_item = Self::main_hand_item(mob);
|
||||
let arrow = ArrowEntity::new_shot_with_weapon(
|
||||
arrow_entity,
|
||||
entity,
|
||||
&projectile,
|
||||
&bow_item,
|
||||
ArrowPickup::Disallowed,
|
||||
);
|
||||
|
||||
// Vanilla scales base damage with power and world difficulty
|
||||
let difficulty = world.level_info.load().difficulty as i32;
|
||||
arrow.set_base_damage_from_mob(Self::ARROW_SPEED, difficulty);
|
||||
|
||||
arrow.set_base_damage(crate::enchantment::EnchantmentHelper::modify_damage(
|
||||
&bow_item,
|
||||
arrow.get_base_damage(),
|
||||
));
|
||||
arrow.punch_level.store(
|
||||
crate::enchantment::EnchantmentHelper::modify_knockback(&bow_item, 0.0) as u8,
|
||||
std::sync::atomic::Ordering::Relaxed,
|
||||
);
|
||||
arrow.apply_on_projectile_spawned(&projectile);
|
||||
if entity.is_on_fire() {
|
||||
arrow.set_flame(true);
|
||||
}
|
||||
|
||||
let mob_pos = entity.pos.load();
|
||||
let target_entity = target.get_entity();
|
||||
@ -88,7 +112,6 @@ impl BowAttackGoal {
|
||||
let horizontal_distance = dx.hypot(dz);
|
||||
|
||||
// Vanilla scales the spread with the world difficulty: 14 - difficulty * 4.
|
||||
let difficulty = world.level_info.load().difficulty as i32;
|
||||
let divergence = f64::from(14 - difficulty * 4);
|
||||
|
||||
arrow.set_velocity(
|
||||
|
||||
@ -75,9 +75,39 @@ impl RangedCrossbowAttackGoal {
|
||||
let target_entity = target.get_entity();
|
||||
let target_pos = target_entity.pos.load();
|
||||
|
||||
let equipment_guard = mob
|
||||
.get_mob_entity()
|
||||
.living_entity
|
||||
.entity_equipment
|
||||
.try_lock();
|
||||
let weapon_opt = equipment_guard
|
||||
.as_ref()
|
||||
.ok()
|
||||
.map(|eq| eq.get(&EquipmentSlot::MAIN_HAND));
|
||||
|
||||
let arrow_entity = Entity::new(world.clone(), mob_pos, &EntityType::ARROW);
|
||||
let projectile = ItemStack::new(1, &Item::ARROW);
|
||||
let arrow = ArrowEntity::new_shot(arrow_entity, entity, &projectile, ArrowPickup::Allowed);
|
||||
let arrow = if let Some(ref weapon) = weapon_opt {
|
||||
ArrowEntity::new_shot_with_weapon(
|
||||
arrow_entity,
|
||||
entity,
|
||||
&projectile,
|
||||
weapon,
|
||||
ArrowPickup::Allowed,
|
||||
)
|
||||
} else {
|
||||
ArrowEntity::new_shot(arrow_entity, entity, &projectile, ArrowPickup::Allowed)
|
||||
};
|
||||
|
||||
if let Some(ref item) = weapon_opt {
|
||||
arrow.set_pierce_level(
|
||||
crate::enchantment::EnchantmentHelper::process_projectile_piercing(item, 0),
|
||||
);
|
||||
}
|
||||
arrow.apply_on_projectile_spawned(&projectile);
|
||||
if entity.is_on_fire() {
|
||||
arrow.set_flame(true);
|
||||
}
|
||||
|
||||
let dx = target_pos.x - mob_pos.x;
|
||||
let dy = (target_pos.y + f64::from(target_entity.entity_dimension.load().height) / 3.0)
|
||||
|
||||
@ -27,7 +27,7 @@ use pumpkin_util::version::JavaMinecraftVersion;
|
||||
use rand::RngExt;
|
||||
use std::sync::Arc;
|
||||
use std::sync::atomic::Ordering::Relaxed;
|
||||
use std::sync::atomic::{AtomicI32, AtomicU8, Ordering};
|
||||
use std::sync::atomic::{AtomicBool, AtomicI32, AtomicU8, Ordering};
|
||||
use uuid::Uuid;
|
||||
|
||||
pub mod bat;
|
||||
@ -82,6 +82,7 @@ pub struct MobEntity {
|
||||
pub love_ticks: AtomicI32,
|
||||
pub breeding_cooldown: AtomicI32,
|
||||
pub breeder: AtomicCell<Option<Uuid>>,
|
||||
pub persistence_required: AtomicBool,
|
||||
mob_flags: AtomicU8,
|
||||
last_sent_yaw: AtomicU8,
|
||||
last_sent_pitch: AtomicU8,
|
||||
@ -178,6 +179,7 @@ impl MobEntity {
|
||||
love_ticks: AtomicI32::new(0),
|
||||
breeding_cooldown: AtomicI32::new(0),
|
||||
breeder: AtomicCell::new(None),
|
||||
persistence_required: AtomicBool::new(false),
|
||||
mob_flags: AtomicU8::new(0),
|
||||
last_sent_yaw: AtomicU8::new(0),
|
||||
last_sent_pitch: AtomicU8::new(0),
|
||||
@ -265,6 +267,9 @@ impl MobEntity {
|
||||
if self.can_pick_up_loot() {
|
||||
nbt.put_bool("CanPickUpLoot", true);
|
||||
}
|
||||
if self.persistence_required.load(Relaxed) {
|
||||
nbt.put_bool("PersistenceRequired", true);
|
||||
}
|
||||
}
|
||||
|
||||
pub fn read_mob_nbt(&self, nbt: &NbtCompound) {
|
||||
@ -277,6 +282,10 @@ impl MobEntity {
|
||||
if let Some(can_pick_up_loot) = nbt.get_bool("CanPickUpLoot") {
|
||||
self.set_can_pick_up_loot(can_pick_up_loot);
|
||||
}
|
||||
if let Some(persistence_required) = nbt.get_bool("PersistenceRequired") {
|
||||
self.persistence_required
|
||||
.store(persistence_required, Relaxed);
|
||||
}
|
||||
}
|
||||
|
||||
pub fn add_goal<G: crate::entity::ai::goal::Goal + 'static>(&self, priority: u8, goal: G) {
|
||||
@ -559,6 +568,48 @@ impl MobEntity {
|
||||
|
||||
false
|
||||
}
|
||||
|
||||
pub fn check_despawn(&self, mob: &dyn Mob) {
|
||||
let entity = &self.living_entity.entity;
|
||||
|
||||
if self.persistence_required.load(Relaxed) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (**entity.custom_name.load()).is_some() {
|
||||
return;
|
||||
}
|
||||
|
||||
let world = entity.world.load();
|
||||
let pos = entity.pos.load();
|
||||
let players = world.players.load();
|
||||
|
||||
let nearest_dist_sq = players
|
||||
.iter()
|
||||
.filter(|p| p.gamemode.load() != pumpkin_util::GameMode::Spectator)
|
||||
.map(|p| {
|
||||
let pp = p.get_entity().pos.load();
|
||||
let dx = pp.x - pos.x;
|
||||
let dy = pp.y - pos.y;
|
||||
let dz = pp.z - pos.z;
|
||||
dx * dx + dy * dy + dz * dz
|
||||
})
|
||||
.fold(f64::MAX, f64::min);
|
||||
|
||||
if nearest_dist_sq == f64::MAX {
|
||||
mob.get_entity().remove();
|
||||
return;
|
||||
}
|
||||
|
||||
if nearest_dist_sq > 128.0 * 128.0 {
|
||||
mob.get_entity().remove();
|
||||
return;
|
||||
}
|
||||
|
||||
if nearest_dist_sq > 32.0 * 32.0 && rand::random::<i32>().wrapping_abs() % 800 == 0 {
|
||||
mob.get_entity().remove();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub trait Mob: EntityBase + Send + Sync {
|
||||
@ -1032,6 +1083,8 @@ impl<T: Mob + Send + 'static> EntityBase for T {
|
||||
}
|
||||
}
|
||||
|
||||
mob_entity.check_despawn(self);
|
||||
|
||||
self.mob_tick(caller);
|
||||
|
||||
let age = mob_entity.living_entity.entity.age.load(Relaxed);
|
||||
|
||||
@ -301,6 +301,26 @@ pub trait EntityBase: Send + Sync + std::any::Any {
|
||||
None
|
||||
}
|
||||
|
||||
fn java_spawn_metadata(&self, version: JavaMinecraftVersion) -> Option<Box<[u8]>> {
|
||||
self.get_mob().map_or_else(
|
||||
|| {
|
||||
let entity = self.get_entity();
|
||||
let shared_flags = entity.flags.load(Ordering::Relaxed);
|
||||
(shared_flags != 0).then(|| {
|
||||
let mut buf = Vec::new();
|
||||
let _ = Metadata::new(
|
||||
pumpkin_data::tracked_data::entity::DATA_SHARED_FLAGS_ID,
|
||||
shared_flags,
|
||||
)
|
||||
.write(&mut buf, &version);
|
||||
buf.put_u8(255);
|
||||
buf.into_boxed_slice()
|
||||
})
|
||||
},
|
||||
|mob| mob.mob_java_spawn_metadata(version),
|
||||
)
|
||||
}
|
||||
|
||||
fn send_bedrock_spawn_packet(&self, client: &BedrockClient) {
|
||||
let entity = self.get_entity();
|
||||
let runtime_id = entity.entity_id as u64;
|
||||
@ -340,10 +360,8 @@ pub trait EntityBase: Send + Sync + std::any::Any {
|
||||
let entity = self.get_entity();
|
||||
let version = client.version.load();
|
||||
let is_mob = entity.entity_type.mob || self.get_mob().is_some();
|
||||
let metadata = self.java_spawn_metadata(version);
|
||||
if version < JavaMinecraftVersion::V_1_19 && is_mob {
|
||||
let metadata = self
|
||||
.get_mob()
|
||||
.and_then(|mob| mob.mob_java_spawn_metadata(version));
|
||||
let spawn_packet = entity.create_spawn_living_packet(metadata.clone());
|
||||
if let Ok(data) = client.serialize_packet(&spawn_packet) {
|
||||
client.try_enqueue_packet(data);
|
||||
@ -361,10 +379,8 @@ pub trait EntityBase: Send + Sync + std::any::Any {
|
||||
if let Ok(data) = client.serialize_packet(&spawn_packet) {
|
||||
client.try_enqueue_packet(data);
|
||||
}
|
||||
if let Some(mob) = self.get_mob()
|
||||
&& let Some(metadata) = mob.mob_java_spawn_metadata(version)
|
||||
{
|
||||
let meta_packet = CSetEntityMetadata::new(entity.entity_id.into(), metadata);
|
||||
if let Some(meta) = metadata {
|
||||
let meta_packet = CSetEntityMetadata::new(entity.entity_id.into(), meta);
|
||||
if let Ok(meta_data) = client.serialize_packet(&meta_packet) {
|
||||
client.try_enqueue_packet(meta_data);
|
||||
}
|
||||
@ -2808,6 +2824,11 @@ impl Entity {
|
||||
}
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub fn is_on_fire(&self) -> bool {
|
||||
self.fire_ticks.load(Ordering::Relaxed) > 0 || self.has_visual_fire.load(Ordering::Relaxed)
|
||||
}
|
||||
|
||||
pub fn get_horizontal_facing(&self) -> HorizontalFacing {
|
||||
let yaw = self.yaw.load();
|
||||
// Use vanilla's formula: floor(angle / 90.0 + 0.5) & 3
|
||||
|
||||
@ -2444,8 +2444,8 @@ mod tests {
|
||||
.iter()
|
||||
.find(|trade| trade.modifier == VillagerTradeModifier::EnchantRandomly)
|
||||
.unwrap();
|
||||
assert!(enchanted_book.wants.item == &Item::EMERALD);
|
||||
assert!(enchanted_book.wants_b.unwrap().item == &Item::BOOK);
|
||||
assert_eq!(enchanted_book.wants.item, &Item::EMERALD);
|
||||
assert_eq!(enchanted_book.wants_b.unwrap().item, &Item::BOOK);
|
||||
assert_eq!(enchanted_book.price_multiplier, 0.2);
|
||||
|
||||
let cartographer = VillagerProfession::Cartographer.trade_set(2).unwrap();
|
||||
|
||||
@ -1277,7 +1277,7 @@ mod tests {
|
||||
#[test]
|
||||
fn invisibility_potion_item_stack_creation() {
|
||||
let potion = create_invisibility_potion();
|
||||
assert!(potion.item == &Item::POTION);
|
||||
assert_eq!(potion.item, &Item::POTION);
|
||||
assert_eq!(potion.item_count, 1);
|
||||
assert!(!potion.patch.is_empty());
|
||||
}
|
||||
|
||||
@ -5158,7 +5158,7 @@ impl Player {
|
||||
.as_any_mut()
|
||||
.downcast_mut::<pumpkin_inventory::anvil::AnvilScreenHandler>()
|
||||
{
|
||||
anvil_handler.update_item_name(packet.item_name.to_string());
|
||||
anvil_handler.set_item_name(packet.item_name, self.has_infinite_materials());
|
||||
}
|
||||
}
|
||||
|
||||
@ -7363,6 +7363,47 @@ impl InventoryPlayer for Player {
|
||||
fn close_screen_handler(&self) {
|
||||
self.close_handled_screen();
|
||||
}
|
||||
|
||||
fn use_anvil(&self) {
|
||||
if let Some(pos) = self.open_container_pos.load() {
|
||||
let world = self.world();
|
||||
let state = world.get_block_state(&pos);
|
||||
let block = pumpkin_data::Block::from_state_id(state.id);
|
||||
if block.has_tag(&pumpkin_data::tag::Block::MINECRAFT_ANVIL) {
|
||||
if !self.has_infinite_materials() && rand::random::<f32>() < 0.12 {
|
||||
if let Some(new_state) =
|
||||
crate::block::blocks::anvil::AnvilBlock::damage(state.id)
|
||||
{
|
||||
world.set_block_state(
|
||||
&pos,
|
||||
new_state,
|
||||
pumpkin_world::world::BlockFlags::NOTIFY_ALL,
|
||||
);
|
||||
world.sync_world_event(
|
||||
pumpkin_data::world::WorldEvent::SoundAnvilUsed,
|
||||
pos,
|
||||
0,
|
||||
);
|
||||
} else {
|
||||
world.set_block_state(
|
||||
&pos,
|
||||
pumpkin_data::BlockStateId::AIR,
|
||||
pumpkin_world::world::BlockFlags::NOTIFY_ALL,
|
||||
);
|
||||
world.sync_world_event(
|
||||
pumpkin_data::world::WorldEvent::SoundAnvilBroken,
|
||||
pos,
|
||||
0,
|
||||
);
|
||||
}
|
||||
} else {
|
||||
world.sync_world_event(pumpkin_data::world::WorldEvent::SoundAnvilUsed, pos, 0);
|
||||
}
|
||||
} else {
|
||||
world.sync_world_event(pumpkin_data::world::WorldEvent::SoundAnvilUsed, pos, 0);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
|
||||
@ -1,12 +1,13 @@
|
||||
use std::sync::Arc;
|
||||
use std::sync::RwLock;
|
||||
use std::sync::atomic::{AtomicBool, AtomicU8, AtomicU32, Ordering};
|
||||
use std::sync::atomic::{AtomicBool, AtomicU8, AtomicU32, AtomicU64, Ordering};
|
||||
|
||||
use crate::entity::projectile::ProjectileHit;
|
||||
use crate::{
|
||||
entity::{Entity, EntityBase, living::LivingEntity, player::Player},
|
||||
server::Server,
|
||||
};
|
||||
use bytes::BufMut;
|
||||
use pumpkin_data::damage::DamageType;
|
||||
use pumpkin_data::data_component_impl::PotionDurationScaleImpl;
|
||||
use pumpkin_data::entity::EntityType;
|
||||
@ -15,11 +16,11 @@ use pumpkin_data::item_stack::ItemStack;
|
||||
use pumpkin_data::particle::Particle;
|
||||
use pumpkin_data::sound::{Sound, SoundCategory};
|
||||
use pumpkin_protocol::IdOr;
|
||||
use pumpkin_protocol::java::client::play::CEntityVelocity;
|
||||
use pumpkin_protocol::java::client::play::CSoundEffect;
|
||||
use pumpkin_protocol::java::client::play::{CEntityVelocity, CSoundEffect, Metadata};
|
||||
use pumpkin_util::math::boundingbox::BoundingBox;
|
||||
use pumpkin_util::math::position::BlockPos;
|
||||
use pumpkin_util::math::vector3::Vector3;
|
||||
use pumpkin_util::version::JavaMinecraftVersion;
|
||||
|
||||
/// Represents the pickup rules for arrows
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
@ -53,9 +54,10 @@ pub struct ArrowEntity {
|
||||
pub entity: Entity,
|
||||
pub owner_id: Option<i32>,
|
||||
pub item_stack: RwLock<ItemStack>,
|
||||
pub base_damage: f64,
|
||||
pub base_damage: AtomicU64,
|
||||
pub pickup: ArrowPickup,
|
||||
pub is_critical: AtomicBool,
|
||||
pub no_physics: AtomicBool,
|
||||
pub pierce_level: AtomicU8,
|
||||
pub punch_level: AtomicU8,
|
||||
pub is_flame: AtomicBool,
|
||||
@ -65,6 +67,8 @@ pub struct ArrowEntity {
|
||||
pub shake_time: AtomicU8,
|
||||
pub has_hit: AtomicBool,
|
||||
pub last_block_pos: Arc<std::sync::RwLock<Option<BlockPos>>>,
|
||||
pub pierced_entities: Arc<RwLock<Vec<i32>>>,
|
||||
pub weapon: RwLock<Option<ItemStack>>,
|
||||
}
|
||||
|
||||
impl ArrowEntity {
|
||||
@ -89,9 +93,10 @@ impl ArrowEntity {
|
||||
entity,
|
||||
owner_id,
|
||||
item_stack: RwLock::new(item_stack.copy_with_count(1)),
|
||||
base_damage: Self::ARROW_BASE_DAMAGE,
|
||||
base_damage: AtomicU64::new(Self::ARROW_BASE_DAMAGE.to_bits()),
|
||||
pickup,
|
||||
is_critical: AtomicBool::new(false),
|
||||
no_physics: AtomicBool::new(false),
|
||||
pierce_level: AtomicU8::new(0),
|
||||
punch_level: AtomicU8::new(0),
|
||||
is_flame: AtomicBool::new(false),
|
||||
@ -101,6 +106,8 @@ impl ArrowEntity {
|
||||
shake_time: AtomicU8::new(0),
|
||||
has_hit: AtomicBool::new(false),
|
||||
last_block_pos: Arc::new(std::sync::RwLock::new(None)),
|
||||
pierced_entities: Arc::new(RwLock::new(Vec::new())),
|
||||
weapon: RwLock::new(None),
|
||||
}
|
||||
}
|
||||
|
||||
@ -128,9 +135,10 @@ impl ArrowEntity {
|
||||
entity,
|
||||
owner_id: Some(shooter.entity_id),
|
||||
item_stack: RwLock::new(item_stack.copy_with_count(1)),
|
||||
base_damage: Self::ARROW_BASE_DAMAGE,
|
||||
base_damage: AtomicU64::new(Self::ARROW_BASE_DAMAGE.to_bits()),
|
||||
pickup,
|
||||
is_critical: AtomicBool::new(false),
|
||||
no_physics: AtomicBool::new(false),
|
||||
pierce_level: AtomicU8::new(0),
|
||||
punch_level: AtomicU8::new(0),
|
||||
is_flame: AtomicBool::new(false),
|
||||
@ -140,9 +148,39 @@ impl ArrowEntity {
|
||||
shake_time: AtomicU8::new(0),
|
||||
has_hit: AtomicBool::new(false),
|
||||
last_block_pos: Arc::new(std::sync::RwLock::new(None)),
|
||||
pierced_entities: Arc::new(RwLock::new(Vec::new())),
|
||||
weapon: RwLock::new(None),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn new_shot_with_weapon(
|
||||
entity: Entity,
|
||||
shooter: &Entity,
|
||||
item_stack: &ItemStack,
|
||||
weapon: &ItemStack,
|
||||
pickup: ArrowPickup,
|
||||
) -> Self {
|
||||
let mut arrow = Self::new_shot(entity, shooter, item_stack, pickup);
|
||||
arrow.weapon = RwLock::new(Some(weapon.copy_with_count(1)));
|
||||
arrow
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub fn get_weapon_item(&self) -> Option<ItemStack> {
|
||||
self.weapon.read().ok().and_then(|w| w.clone())
|
||||
}
|
||||
|
||||
/// Applies projectile-spawned enchantment effects matching vanilla `Projectile::applyOnProjectileSpawned`.
|
||||
pub fn apply_on_projectile_spawned(&self, pickup_item_stack: &ItemStack) {
|
||||
let weapon = self.weapon.read().ok().and_then(|w| w.clone());
|
||||
super::apply_on_projectile_spawned(
|
||||
self.get_entity(),
|
||||
pickup_item_stack,
|
||||
weapon.as_ref(),
|
||||
Some(self),
|
||||
);
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub const fn entity_type_for_item(item: &'static Item) -> &'static EntityType {
|
||||
if item.id == Item::SPECTRAL_ARROW.id {
|
||||
@ -193,6 +231,40 @@ impl ArrowEntity {
|
||||
damage_succeeded
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub fn get_effect_color(item_stack: &ItemStack) -> i32 {
|
||||
if let Some(pc) =
|
||||
item_stack.get_data_component::<pumpkin_data::data_component_impl::PotionContentsImpl>()
|
||||
{
|
||||
if let Some(color) = pc.custom_color {
|
||||
return color;
|
||||
}
|
||||
let effects = crate::item::potion::PotionContents::read_potion_effects(item_stack);
|
||||
if effects.is_empty() {
|
||||
return -1;
|
||||
}
|
||||
let mut r = 0;
|
||||
let mut g = 0;
|
||||
let mut b = 0;
|
||||
let mut total = 0;
|
||||
for (effect, _, amplifier, _, _, _) in effects {
|
||||
let color = effect.color;
|
||||
let weight = (amplifier as i32) + 1;
|
||||
r += weight * ((color >> 16) & 0xFF);
|
||||
g += weight * ((color >> 8) & 0xFF);
|
||||
b += weight * (color & 0xFF);
|
||||
total += weight;
|
||||
}
|
||||
if total == 0 {
|
||||
-1
|
||||
} else {
|
||||
((r / total) << 16) | ((g / total) << 8) | (b / total)
|
||||
}
|
||||
} else {
|
||||
-1
|
||||
}
|
||||
}
|
||||
|
||||
pub fn set_velocity_from_rotation(
|
||||
&self,
|
||||
pitch: f32,
|
||||
@ -240,16 +312,88 @@ impl ArrowEntity {
|
||||
);
|
||||
}
|
||||
|
||||
fn get_flags(&self) -> u8 {
|
||||
let mut flags = 0u8;
|
||||
if self.is_critical.load(Ordering::Relaxed) {
|
||||
flags |= 0x01;
|
||||
}
|
||||
if self.no_physics.load(Ordering::Relaxed) {
|
||||
flags |= 0x02;
|
||||
}
|
||||
flags
|
||||
}
|
||||
|
||||
pub fn set_critical(&self, critical: bool) {
|
||||
self.is_critical.store(critical, Ordering::Relaxed);
|
||||
let flags = self.get_flags();
|
||||
self.entity.send_meta_data(
|
||||
&[Metadata::new(
|
||||
pumpkin_data::tracked_data::abstract_arrow::ID_FLAGS,
|
||||
flags,
|
||||
)],
|
||||
None,
|
||||
);
|
||||
}
|
||||
|
||||
pub fn set_no_physics(&self, no_physics: bool) {
|
||||
self.no_physics.store(no_physics, Ordering::Relaxed);
|
||||
let flags = self.get_flags();
|
||||
self.entity.send_meta_data(
|
||||
&[Metadata::new(
|
||||
pumpkin_data::tracked_data::abstract_arrow::ID_FLAGS,
|
||||
flags,
|
||||
)],
|
||||
None,
|
||||
);
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub fn is_no_physics(&self) -> bool {
|
||||
self.no_physics.load(Ordering::Relaxed)
|
||||
}
|
||||
|
||||
pub fn set_base_damage_from_mob(&self, power: f64, difficulty_id: i32) {
|
||||
fn next_triangular(mode: f64, deviation: f64) -> f64 {
|
||||
deviation.mul_add(rand::random::<f64>() - rand::random::<f64>(), mode)
|
||||
}
|
||||
let diff_factor = f64::from(difficulty_id) * 0.11;
|
||||
let base = power * 2.0 + next_triangular(diff_factor, 0.57425);
|
||||
self.set_base_damage(base);
|
||||
}
|
||||
|
||||
pub fn set_pierce_level(&self, level: u8) {
|
||||
self.pierce_level.store(level, Ordering::Relaxed);
|
||||
self.entity.send_meta_data(
|
||||
&[Metadata::new(
|
||||
pumpkin_data::tracked_data::abstract_arrow::PIERCE_LEVEL,
|
||||
level,
|
||||
)],
|
||||
None,
|
||||
);
|
||||
}
|
||||
|
||||
pub const fn set_base_damage(&self, _damage: f64) {
|
||||
// TODO: implement this
|
||||
#[must_use]
|
||||
pub fn get_base_damage(&self) -> f64 {
|
||||
f64::from_bits(self.base_damage.load(Ordering::Relaxed))
|
||||
}
|
||||
|
||||
pub fn set_base_damage(&self, damage: f64) {
|
||||
self.base_damage.store(damage.to_bits(), Ordering::Relaxed);
|
||||
}
|
||||
|
||||
pub fn set_flame(&self, flame: bool) {
|
||||
self.is_flame.store(flame, Ordering::Relaxed);
|
||||
if flame {
|
||||
self.entity.set_on_fire_for(100.0);
|
||||
self.entity.set_on_fire(true);
|
||||
} else {
|
||||
self.entity.extinguish();
|
||||
}
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub fn is_on_fire(&self) -> bool {
|
||||
self.entity.is_on_fire() || self.is_flame.load(Ordering::Relaxed)
|
||||
}
|
||||
|
||||
#[allow(dead_code)]
|
||||
@ -275,6 +419,16 @@ impl EntityBase for ArrowEntity {
|
||||
.read()
|
||||
.unwrap_or_else(std::sync::PoisonError::into_inner);
|
||||
Self::write_item_stack_nbt(&item_stack, nbt);
|
||||
nbt.put_bool("crit", self.is_critical.load(Ordering::Relaxed));
|
||||
nbt.put_double("damage", self.get_base_damage());
|
||||
nbt.put_bool("inGround", self.in_ground.load(Ordering::Relaxed));
|
||||
nbt.put_int("life", self.life.load(Ordering::Relaxed) as i32);
|
||||
nbt.put_byte("shake", self.shake_time.load(Ordering::Relaxed) as i8);
|
||||
nbt.put_byte(
|
||||
"PierceLevel",
|
||||
self.pierce_level.load(Ordering::Relaxed) as i8,
|
||||
);
|
||||
nbt.put_byte("pickup", self.pickup.to_byte() as i8);
|
||||
}
|
||||
|
||||
fn read_custom_nbt(&self, nbt: &pumpkin_nbt::compound::NbtCompound) {
|
||||
@ -284,12 +438,183 @@ impl EntityBase for ArrowEntity {
|
||||
.write()
|
||||
.unwrap_or_else(std::sync::PoisonError::into_inner) = item_stack;
|
||||
}
|
||||
if let Some(crit) = nbt.get_bool("crit") {
|
||||
self.is_critical.store(crit, Ordering::Relaxed);
|
||||
}
|
||||
if let Some(damage) = nbt.get_double("damage") {
|
||||
self.set_base_damage(damage);
|
||||
}
|
||||
if let Some(in_ground) = nbt
|
||||
.get_bool("inGround")
|
||||
.or_else(|| nbt.get_byte("inGround").map(|b| b != 0))
|
||||
{
|
||||
self.in_ground.store(in_ground, Ordering::Relaxed);
|
||||
}
|
||||
if let Some(life) = nbt
|
||||
.get_int("life")
|
||||
.or_else(|| nbt.get_short("life").map(i32::from))
|
||||
{
|
||||
self.life.store(life.max(0) as u32, Ordering::Relaxed);
|
||||
}
|
||||
if let Some(shake) = nbt.get_byte("shake") {
|
||||
self.shake_time.store(shake.max(0) as u8, Ordering::Relaxed);
|
||||
}
|
||||
if let Some(pierce) = nbt.get_byte("PierceLevel") {
|
||||
self.pierce_level
|
||||
.store(pierce.max(0) as u8, Ordering::Relaxed);
|
||||
}
|
||||
}
|
||||
|
||||
fn init_data_tracker(&self) {
|
||||
let entity = self.get_entity();
|
||||
let flags = self.get_flags();
|
||||
let pierce = self.pierce_level.load(Ordering::Relaxed);
|
||||
let in_ground = self.in_ground.load(Ordering::Relaxed);
|
||||
|
||||
if entity.entity_type.id == EntityType::SPECTRAL_ARROW.id {
|
||||
entity.send_meta_data(
|
||||
&[
|
||||
Metadata::new(pumpkin_data::tracked_data::spectral_arrow::ID_FLAGS, flags),
|
||||
Metadata::new(
|
||||
pumpkin_data::tracked_data::spectral_arrow::PIERCE_LEVEL,
|
||||
pierce,
|
||||
),
|
||||
],
|
||||
None,
|
||||
);
|
||||
entity.send_meta_data(
|
||||
&[Metadata::new(
|
||||
pumpkin_data::tracked_data::spectral_arrow::IN_GROUND,
|
||||
in_ground,
|
||||
)],
|
||||
None,
|
||||
);
|
||||
} else {
|
||||
let item_stack = self
|
||||
.item_stack
|
||||
.read()
|
||||
.unwrap_or_else(std::sync::PoisonError::into_inner);
|
||||
let color = Self::get_effect_color(&item_stack);
|
||||
entity.send_meta_data(
|
||||
&[
|
||||
Metadata::new(pumpkin_data::tracked_data::arrow::ID_FLAGS, flags),
|
||||
Metadata::new(pumpkin_data::tracked_data::arrow::PIERCE_LEVEL, pierce),
|
||||
],
|
||||
None,
|
||||
);
|
||||
entity.send_meta_data(
|
||||
&[Metadata::new(
|
||||
pumpkin_data::tracked_data::arrow::IN_GROUND,
|
||||
in_ground,
|
||||
)],
|
||||
None,
|
||||
);
|
||||
entity.send_meta_data(
|
||||
&[Metadata::new(
|
||||
pumpkin_data::tracked_data::arrow::ID_EFFECT_COLOR,
|
||||
color,
|
||||
)],
|
||||
None,
|
||||
);
|
||||
}
|
||||
|
||||
if self.is_on_fire() {
|
||||
entity.set_on_fire(true);
|
||||
}
|
||||
}
|
||||
|
||||
fn java_spawn_metadata(&self, version: JavaMinecraftVersion) -> Option<Box<[u8]>> {
|
||||
let entity = self.get_entity();
|
||||
let flags = self.get_flags();
|
||||
let pierce = self.pierce_level.load(Ordering::Relaxed);
|
||||
let in_ground = self.in_ground.load(Ordering::Relaxed);
|
||||
let shared_flags = entity.flags.load(Ordering::Relaxed);
|
||||
|
||||
let mut buf = Vec::new();
|
||||
if shared_flags != 0 {
|
||||
let _ = Metadata::new(
|
||||
pumpkin_data::tracked_data::entity::DATA_SHARED_FLAGS_ID,
|
||||
shared_flags,
|
||||
)
|
||||
.write(&mut buf, &version);
|
||||
}
|
||||
if entity.entity_type.id == EntityType::SPECTRAL_ARROW.id {
|
||||
let _ = Metadata::new(pumpkin_data::tracked_data::spectral_arrow::ID_FLAGS, flags)
|
||||
.write(&mut buf, &version);
|
||||
let _ = Metadata::new(
|
||||
pumpkin_data::tracked_data::spectral_arrow::PIERCE_LEVEL,
|
||||
pierce,
|
||||
)
|
||||
.write(&mut buf, &version);
|
||||
let _ = Metadata::new(
|
||||
pumpkin_data::tracked_data::spectral_arrow::IN_GROUND,
|
||||
in_ground,
|
||||
)
|
||||
.write(&mut buf, &version);
|
||||
} else {
|
||||
let item_stack = self
|
||||
.item_stack
|
||||
.read()
|
||||
.unwrap_or_else(std::sync::PoisonError::into_inner);
|
||||
let color = Self::get_effect_color(&item_stack);
|
||||
let _ = Metadata::new(pumpkin_data::tracked_data::arrow::ID_FLAGS, flags)
|
||||
.write(&mut buf, &version);
|
||||
let _ = Metadata::new(pumpkin_data::tracked_data::arrow::PIERCE_LEVEL, pierce)
|
||||
.write(&mut buf, &version);
|
||||
let _ = Metadata::new(pumpkin_data::tracked_data::arrow::IN_GROUND, in_ground)
|
||||
.write(&mut buf, &version);
|
||||
if color != -1 {
|
||||
let _ = Metadata::new(pumpkin_data::tracked_data::arrow::ID_EFFECT_COLOR, color)
|
||||
.write(&mut buf, &version);
|
||||
}
|
||||
}
|
||||
(!buf.is_empty()).then(|| {
|
||||
buf.put_u8(255);
|
||||
buf.into_boxed_slice()
|
||||
})
|
||||
}
|
||||
|
||||
#[allow(clippy::too_many_lines)]
|
||||
fn tick(&self, caller: &dyn EntityBase, _server: &Server) {
|
||||
let entity = self.get_entity();
|
||||
let world = entity.world.load();
|
||||
|
||||
// Fire & Extinguish logic
|
||||
let mut fire_ticks = entity.fire_ticks.load(Ordering::Relaxed);
|
||||
let touching_water = entity.touching_water.load(Ordering::Relaxed);
|
||||
let in_water = touching_water || entity.is_in_water();
|
||||
let block_pos = entity.block_pos.load();
|
||||
let in_rain = world.is_raining_at(&block_pos);
|
||||
|
||||
if in_water || in_rain {
|
||||
if entity.is_on_fire() || self.is_flame.load(Ordering::Relaxed) {
|
||||
entity.extinguish();
|
||||
self.is_flame.store(false, Ordering::Relaxed);
|
||||
}
|
||||
} else if fire_ticks > 0 {
|
||||
fire_ticks -= 1;
|
||||
entity.fire_ticks.store(fire_ticks, Ordering::Relaxed);
|
||||
if fire_ticks <= 0 && self.is_flame.load(Ordering::Relaxed) {
|
||||
self.is_flame.store(false, Ordering::Relaxed);
|
||||
entity.set_on_fire(false);
|
||||
}
|
||||
}
|
||||
|
||||
// Check if arrow enters lava or fire block
|
||||
let current_block = world.get_block(&block_pos);
|
||||
if current_block == &pumpkin_data::Block::LAVA {
|
||||
entity.set_on_fire_for(15.0);
|
||||
self.is_flame.store(true, Ordering::Relaxed);
|
||||
} else if current_block == &pumpkin_data::Block::FIRE
|
||||
|| current_block == &pumpkin_data::Block::SOUL_FIRE
|
||||
{
|
||||
entity.set_on_fire_for(8.0);
|
||||
self.is_flame.store(true, Ordering::Relaxed);
|
||||
}
|
||||
|
||||
let is_on_fire = entity.is_on_fire() || self.is_flame.load(Ordering::Relaxed);
|
||||
entity.set_on_fire(is_on_fire);
|
||||
|
||||
// Handle shake time
|
||||
let shake = self.shake_time.load(Ordering::Relaxed);
|
||||
if shake > 0 {
|
||||
@ -297,6 +622,30 @@ impl EntityBase for ArrowEntity {
|
||||
}
|
||||
|
||||
if self.in_ground.load(Ordering::Relaxed) {
|
||||
// Check if the block we are stuck into was broken / turned to air
|
||||
let last_pos = *self
|
||||
.last_block_pos
|
||||
.read()
|
||||
.unwrap_or_else(std::sync::PoisonError::into_inner);
|
||||
if let Some(pos) = last_pos {
|
||||
let block = world.get_block(&pos);
|
||||
if block.is_air() {
|
||||
self.in_ground.store(false, Ordering::Relaxed);
|
||||
entity.send_meta_data(
|
||||
&[Metadata::new(
|
||||
pumpkin_data::tracked_data::abstract_arrow::IN_GROUND,
|
||||
false,
|
||||
)],
|
||||
None,
|
||||
);
|
||||
let mut vel = entity.velocity.load();
|
||||
vel.x *= rand::random::<f64>() * 0.2;
|
||||
vel.y *= rand::random::<f64>() * 0.2;
|
||||
vel.z *= rand::random::<f64>() * 0.2;
|
||||
entity.velocity.store(vel);
|
||||
}
|
||||
}
|
||||
|
||||
// Increment in-ground time and life
|
||||
let _in_ground_time = self.in_ground_time.fetch_add(1, Ordering::Relaxed);
|
||||
let life = self.life.fetch_add(1, Ordering::Relaxed);
|
||||
@ -316,7 +665,7 @@ impl EntityBase for ArrowEntity {
|
||||
velocity.y -= Self::GRAVITY;
|
||||
|
||||
// Apply inertia (air resistance or water drag)
|
||||
let inertia = if entity.touching_water.load(Ordering::Relaxed) {
|
||||
let inertia = if in_water {
|
||||
Self::WATER_INERTIA
|
||||
} else {
|
||||
Self::AIR_INERTIA
|
||||
@ -336,17 +685,55 @@ impl EntityBase for ArrowEntity {
|
||||
let new_pos = start_pos.add(&velocity);
|
||||
entity.set_pos(new_pos);
|
||||
|
||||
// Spawn critical particle trail while arrow is flying and critical
|
||||
if self.is_critical.load(Ordering::Relaxed) {
|
||||
// Spawn particles while arrow is flying
|
||||
if in_water {
|
||||
for i in 0..4 {
|
||||
let factor = 0.25 * f64::from(i);
|
||||
world.spawn_particle(
|
||||
Vector3::new(
|
||||
new_pos.x - velocity.x * factor,
|
||||
new_pos.y - velocity.y * factor,
|
||||
new_pos.z - velocity.z * factor,
|
||||
),
|
||||
Vector3::new(velocity.x as f32, velocity.y as f32, velocity.z as f32),
|
||||
0.0,
|
||||
1,
|
||||
Particle::Bubble,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
if is_on_fire {
|
||||
world.spawn_particle(
|
||||
entity.pos.load(),
|
||||
Vector3::new(0.0f32, 0.0f32, 0.0f32),
|
||||
0.0,
|
||||
1,
|
||||
Particle::Crit,
|
||||
Particle::Flame,
|
||||
);
|
||||
}
|
||||
|
||||
if self.is_critical.load(Ordering::Relaxed) {
|
||||
for i in 0..4 {
|
||||
let factor = f64::from(i) / 4.0;
|
||||
world.spawn_particle(
|
||||
Vector3::new(
|
||||
start_pos.x + velocity.x * factor,
|
||||
start_pos.y + velocity.y * factor,
|
||||
start_pos.z + velocity.z * factor,
|
||||
),
|
||||
Vector3::new(
|
||||
-velocity.x as f32,
|
||||
(-velocity.y + 0.2) as f32,
|
||||
-velocity.z as f32,
|
||||
),
|
||||
0.0,
|
||||
1,
|
||||
Particle::Crit,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// Broadcast velocity update
|
||||
let packet = CEntityVelocity::new(entity.entity_id.into(), velocity);
|
||||
|
||||
@ -421,12 +808,26 @@ impl EntityBase for ArrowEntity {
|
||||
|
||||
// Handle hit
|
||||
if let Some(h) = hit {
|
||||
// Ensure hit is only processed once
|
||||
if self.has_hit.swap(true, Ordering::SeqCst) {
|
||||
return;
|
||||
match h {
|
||||
ProjectileHit::Block { .. } => {
|
||||
if self.has_hit.swap(true, Ordering::SeqCst) {
|
||||
return;
|
||||
}
|
||||
caller.on_hit(h);
|
||||
}
|
||||
ProjectileHit::Entity { .. } => {
|
||||
let pierce = self.pierce_level.load(Ordering::Relaxed);
|
||||
let pierced_len = self
|
||||
.pierced_entities
|
||||
.read()
|
||||
.unwrap_or_else(std::sync::PoisonError::into_inner)
|
||||
.len();
|
||||
if pierced_len >= pierce as usize && self.has_hit.swap(true, Ordering::SeqCst) {
|
||||
return;
|
||||
}
|
||||
caller.on_hit(h);
|
||||
}
|
||||
}
|
||||
|
||||
caller.on_hit(h);
|
||||
}
|
||||
}
|
||||
|
||||
@ -469,6 +870,13 @@ impl EntityBase for ArrowEntity {
|
||||
.unwrap_or_else(std::sync::PoisonError::into_inner) = Some(pos);
|
||||
|
||||
let block = world.get_block(&pos);
|
||||
let state = world.get_block_state(&pos);
|
||||
if let Some(server) = world.server.upgrade() {
|
||||
world
|
||||
.block_registry
|
||||
.on_projectile_hit(block, &world, self, &pos, state, &hit_pos, &server);
|
||||
}
|
||||
|
||||
if block == &pumpkin_data::Block::TARGET
|
||||
&& let Some(player) = self.owner_id.and_then(|id| world.get_player_by_id(id))
|
||||
{
|
||||
@ -477,83 +885,122 @@ impl EntityBase for ArrowEntity {
|
||||
);
|
||||
}
|
||||
|
||||
// Stop the arrow
|
||||
// Stop the arrow with slight position offset backwards
|
||||
let velocity = entity.velocity.load();
|
||||
let norm_dir = Vector3::new(
|
||||
velocity.x.signum(),
|
||||
velocity.y.signum(),
|
||||
velocity.z.signum(),
|
||||
);
|
||||
let offset = norm_dir.multiply(0.05, 0.05, 0.05);
|
||||
entity.set_pos(hit_pos.sub(&offset));
|
||||
entity.velocity.store(Vector3::new(0.0, 0.0, 0.0));
|
||||
entity.set_pos(hit_pos);
|
||||
|
||||
// Play sound
|
||||
// Notify client that arrow is in ground
|
||||
entity.send_meta_data(
|
||||
&[Metadata::new(
|
||||
pumpkin_data::tracked_data::abstract_arrow::IN_GROUND,
|
||||
true,
|
||||
)],
|
||||
None,
|
||||
);
|
||||
|
||||
// Play sound with vanilla pitch formula
|
||||
let sound_pitch = 1.2 / (rand::random::<f32>() * 0.2 + 0.9);
|
||||
let sound_packet = CSoundEffect::new(
|
||||
IdOr::Id(Sound::EntityArrowHit as u16),
|
||||
SoundCategory::Neutral,
|
||||
&hit_pos,
|
||||
1.0,
|
||||
1.0,
|
||||
sound_pitch,
|
||||
0.0,
|
||||
);
|
||||
let chunk_pos = entity.chunk_pos.load();
|
||||
world.broadcast_to_chunk(chunk_pos, &sound_packet);
|
||||
|
||||
// Reset critical flag
|
||||
self.is_critical.store(false, Ordering::Relaxed);
|
||||
// Reset critical flag and pierce level
|
||||
self.set_critical(false);
|
||||
self.set_pierce_level(0);
|
||||
self.pierced_entities
|
||||
.write()
|
||||
.unwrap_or_else(std::sync::PoisonError::into_inner)
|
||||
.clear();
|
||||
}
|
||||
ProjectileHit::Entity {
|
||||
entity: target,
|
||||
hit_pos,
|
||||
..
|
||||
} => {
|
||||
let target_entity_id = target.get_entity().entity_id;
|
||||
self.pierced_entities
|
||||
.write()
|
||||
.unwrap_or_else(std::sync::PoisonError::into_inner)
|
||||
.push(target_entity_id);
|
||||
|
||||
// Calculate damage
|
||||
let velocity = entity.velocity.load();
|
||||
let power = velocity.length();
|
||||
let mut damage = (power * self.base_damage).ceil() as i32;
|
||||
let mut damage = (power * self.get_base_damage()).ceil() as i32;
|
||||
|
||||
// Apply critical hit bonus
|
||||
if self.is_critical.load(Ordering::Relaxed) {
|
||||
let bonus = (rand::random::<u32>() % (damage / 2 + 2) as u32) as i32;
|
||||
let bonus = (rand::random::<u32>() % (damage.max(0) / 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);
|
||||
|
||||
let is_enderman =
|
||||
target.get_entity().entity_type == &pumpkin_data::entity::EntityType::ENDERMAN;
|
||||
let is_on_fire = entity.is_on_fire() || self.is_flame.load(Ordering::Relaxed);
|
||||
if is_on_fire && !is_enderman {
|
||||
target.get_entity().set_on_fire_for(5.0);
|
||||
}
|
||||
|
||||
let punch = self.punch_level.load(Ordering::Relaxed);
|
||||
let is_spectral = entity.entity_type.id == EntityType::SPECTRAL_ARROW.id;
|
||||
let entity_type: &'static EntityType = entity.entity_type;
|
||||
let owner_id = self.owner_id;
|
||||
let pierce = self.pierce_level.load(Ordering::Relaxed);
|
||||
|
||||
let owner_entity = owner_id.and_then(|id| world.get_entity_by_id(id));
|
||||
|
||||
let damage_succeeded = target.damage_with_context(
|
||||
target.as_ref(),
|
||||
damage as f32,
|
||||
DamageType::ARROW,
|
||||
Some(hit_pos),
|
||||
None,
|
||||
owner_entity.as_deref(),
|
||||
None,
|
||||
);
|
||||
|
||||
if let Some(living) = target.get_living_entity() {
|
||||
if punch > 0
|
||||
&& let Some(owner_id) = owner_id
|
||||
&& let Some(owner_entity) = world.get_entity_by_id(owner_id)
|
||||
{
|
||||
crate::entity::combat::handle_knockback(
|
||||
owner_entity.get_entity(),
|
||||
target.as_ref(),
|
||||
f64::from(punch) * 0.6,
|
||||
if punch > 0 {
|
||||
let norm = Vector3::new(velocity.x, 0.0, velocity.z).normalize();
|
||||
let push_scale = f64::from(punch) * 0.6;
|
||||
target.get_entity().velocity.store(
|
||||
target.get_entity().velocity.load().add(&Vector3::new(
|
||||
norm.x * push_scale,
|
||||
0.1,
|
||||
norm.z * push_scale,
|
||||
)),
|
||||
);
|
||||
}
|
||||
|
||||
// Play hit sound
|
||||
let sound_pitch = 1.2 / (rand::random::<f32>() * 0.2 + 0.9);
|
||||
let sound_packet = CSoundEffect::new(
|
||||
IdOr::Id(Sound::EntityArrowHit as u16),
|
||||
SoundCategory::Neutral,
|
||||
&hit_pos,
|
||||
1.0,
|
||||
1.0,
|
||||
sound_pitch,
|
||||
0.0,
|
||||
);
|
||||
world.broadcast_packet_all(&sound_packet);
|
||||
|
||||
if Self::should_apply_post_hurt_effects(damage_succeeded) {
|
||||
let item_stack = ItemStack::new(1, Self::default_item(entity_type));
|
||||
let item_stack = self
|
||||
.item_stack
|
||||
.read()
|
||||
.unwrap_or_else(std::sync::PoisonError::into_inner);
|
||||
let scale = item_stack
|
||||
.get_data_component::<PotionDurationScaleImpl>()
|
||||
.map_or(1.0, |component| component.scale);
|
||||
@ -571,8 +1018,12 @@ impl EntityBase for ArrowEntity {
|
||||
}
|
||||
|
||||
// Check pierce level
|
||||
if pierce == 0 {
|
||||
// No piercing - remove arrow
|
||||
let pierced_count = self
|
||||
.pierced_entities
|
||||
.read()
|
||||
.unwrap_or_else(std::sync::PoisonError::into_inner)
|
||||
.len();
|
||||
if pierced_count > pierce as usize {
|
||||
entity.remove();
|
||||
}
|
||||
}
|
||||
@ -638,11 +1089,27 @@ impl ArrowEntity {
|
||||
return true;
|
||||
}
|
||||
|
||||
// Skip other arrows, item entities, and falling block entities
|
||||
// Skip already pierced entities
|
||||
if self
|
||||
.pierced_entities
|
||||
.read()
|
||||
.unwrap_or_else(std::sync::PoisonError::into_inner)
|
||||
.contains(&other_ent.entity_id)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
// Skip dead entities
|
||||
if !other_ent.is_alive() {
|
||||
return true;
|
||||
}
|
||||
|
||||
// Skip other arrows, item entities, falling block entities, and area effect clouds
|
||||
if (other_ent.entity_type == &pumpkin_data::entity::EntityType::ARROW
|
||||
|| other_ent.entity_type == &pumpkin_data::entity::EntityType::SPECTRAL_ARROW)
|
||||
|| other_ent.entity_type == &pumpkin_data::entity::EntityType::ITEM
|
||||
|| other_ent.entity_type == &pumpkin_data::entity::EntityType::FALLING_BLOCK
|
||||
|| other_ent.entity_type == &pumpkin_data::entity::EntityType::AREA_EFFECT_CLOUD
|
||||
{
|
||||
return true;
|
||||
}
|
||||
@ -793,4 +1260,24 @@ mod tests {
|
||||
assert!(!ArrowEntity::should_apply_post_hurt_effects(false));
|
||||
assert!(ArrowEntity::should_apply_post_hurt_effects(true));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn tipped_arrow_effect_color_uses_custom_color_or_effects() {
|
||||
let tipped = tipped_payload(1);
|
||||
let color = ArrowEntity::get_effect_color(&tipped);
|
||||
assert_eq!(color, 0x123456);
|
||||
|
||||
let normal = ItemStack::new(1, &Item::ARROW);
|
||||
let normal_color = ArrowEntity::get_effect_color(&normal);
|
||||
assert_eq!(normal_color, -1);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn power_enchantment_damage_formula_matches_vanilla() {
|
||||
let base_damage = 2.0;
|
||||
let power_1 = base_damage + 1.0 * 0.5 + 0.5;
|
||||
let power_5 = base_damage + 5.0 * 0.5 + 0.5;
|
||||
assert_eq!(power_1, 3.0);
|
||||
assert_eq!(power_5, 5.0);
|
||||
}
|
||||
}
|
||||
|
||||
@ -26,6 +26,8 @@ pub mod trident;
|
||||
pub mod wind_charge;
|
||||
pub mod wither_skull;
|
||||
|
||||
use pumpkin_data::item_stack::ItemStack;
|
||||
|
||||
#[must_use]
|
||||
pub fn is_projectile(entity_type: &EntityType) -> bool {
|
||||
*entity_type == EntityType::ARROW
|
||||
@ -45,6 +47,30 @@ pub fn is_projectile(entity_type: &EntityType) -> bool {
|
||||
|| *entity_type == EntityType::LLAMA_SPIT
|
||||
}
|
||||
|
||||
/// Helper to apply projectile spawned enchantment effects matching vanilla `Projectile::applyOnProjectileSpawned`.
|
||||
pub fn apply_on_projectile_spawned(
|
||||
projectile_entity: &Entity,
|
||||
pickup_item_stack: &ItemStack,
|
||||
weapon: Option<&ItemStack>,
|
||||
arrow: Option<&arrow::ArrowEntity>,
|
||||
) {
|
||||
crate::enchantment::EnchantmentHelper::on_projectile_spawned(
|
||||
pickup_item_stack,
|
||||
projectile_entity,
|
||||
arrow,
|
||||
);
|
||||
if let Some(weapon) = weapon
|
||||
&& weapon.item_count > 0
|
||||
&& weapon.item.id != pickup_item_stack.item.id
|
||||
{
|
||||
crate::enchantment::EnchantmentHelper::on_projectile_spawned(
|
||||
weapon,
|
||||
projectile_entity,
|
||||
arrow,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
pub struct ThrownItemEntity {
|
||||
pub entity: Entity,
|
||||
pub owner_id: Option<i32>,
|
||||
@ -212,6 +238,16 @@ impl ThrownItemEntity {
|
||||
return;
|
||||
}
|
||||
|
||||
if let ProjectileHit::Block { pos, hit_pos, .. } = &h {
|
||||
let block = world.get_block(pos);
|
||||
let state = world.get_block_state(pos);
|
||||
if let Some(server) = world.server.upgrade() {
|
||||
world
|
||||
.block_registry
|
||||
.on_projectile_hit(block, &world, caller, pos, state, hit_pos, &server);
|
||||
}
|
||||
}
|
||||
|
||||
// Just trigger hit effects and remove
|
||||
caller.on_hit(h);
|
||||
entity.remove();
|
||||
|
||||
@ -78,6 +78,11 @@ impl TridentEntity {
|
||||
}
|
||||
}
|
||||
|
||||
/// Applies projectile-spawned enchantment effects matching vanilla `Projectile::applyOnProjectileSpawned`.
|
||||
pub fn apply_on_projectile_spawned(&self, pickup_item_stack: &ItemStack) {
|
||||
super::apply_on_projectile_spawned(self.get_entity(), pickup_item_stack, None, None);
|
||||
}
|
||||
|
||||
pub fn set_velocity_from_rotation(
|
||||
&self,
|
||||
pitch: f32,
|
||||
@ -304,6 +309,14 @@ impl EntityBase for TridentEntity {
|
||||
.write()
|
||||
.unwrap_or_else(std::sync::PoisonError::into_inner) = Some(pos);
|
||||
|
||||
let block = world.get_block(&pos);
|
||||
let state = world.get_block_state(&pos);
|
||||
if let Some(server) = world.server.upgrade() {
|
||||
world
|
||||
.block_registry
|
||||
.on_projectile_hit(block, &world, self, &pos, state, &hit_pos, &server);
|
||||
}
|
||||
|
||||
// Stop the trident
|
||||
entity.velocity.store(Vector3::new(0.0, 0.0, 0.0));
|
||||
entity.set_pos(hit_pos);
|
||||
|
||||
@ -1,10 +1,10 @@
|
||||
use std::any::Any;
|
||||
use std::sync::Arc;
|
||||
use std::sync::atomic::Ordering;
|
||||
|
||||
use crate::entity::EntityBase;
|
||||
use crate::entity::player::Player;
|
||||
use crate::entity::projectile::arrow::{ArrowEntity, ArrowPickup};
|
||||
use crate::entity::{Entity, EntityBase};
|
||||
use crate::entity::projectile::arrow::ArrowEntity;
|
||||
use crate::item::items::projectile_weapon::ProjectileWeaponItem;
|
||||
use crate::item::{ItemBehaviour, ItemMetadata};
|
||||
use pumpkin_data::item::Item;
|
||||
use pumpkin_data::item_stack::ItemStack;
|
||||
@ -41,8 +41,8 @@ impl ItemBehaviour for BowItem {
|
||||
.set_active_hand(pumpkin_util::Hand::Right, stack, Self::USE_DURATION);
|
||||
}
|
||||
|
||||
fn on_stopped_using(&self, _stack: &ItemStack, player: &Player) {
|
||||
Self::release_bow(player);
|
||||
fn on_stopped_using(&self, stack: &ItemStack, player: &Player) {
|
||||
Self::release_bow(player, stack);
|
||||
}
|
||||
|
||||
fn get_use_duration(&self) -> i32 {
|
||||
@ -58,10 +58,10 @@ impl BowItem {
|
||||
/// The maximum number of ticks a bow can be drawn for
|
||||
pub const USE_DURATION: i32 = 72000;
|
||||
const MAX_DRAW_DURATION: f32 = 20.0;
|
||||
const ARROW_SPEED_MULTIPLIER: f32 = 3.0;
|
||||
pub const ARROW_SPEED_MULTIPLIER: f32 = 3.0;
|
||||
|
||||
/// Called when the player releases the bow
|
||||
pub fn release_bow(player: &Player) {
|
||||
pub fn release_bow(player: &Player, weapon: &ItemStack) {
|
||||
// Get the used ticks
|
||||
let use_ticks = player.living_entity.item_use_time.load(Ordering::Relaxed);
|
||||
let use_ticks = Self::USE_DURATION - use_ticks;
|
||||
@ -92,18 +92,24 @@ impl BowItem {
|
||||
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.get_data_component::<pumpkin_data::data_component_impl::EnchantmentsImpl>()
|
||||
{
|
||||
has_infinity = enchantments
|
||||
.enchantment
|
||||
.iter()
|
||||
.any(|(e, _)| **e == pumpkin_data::Enchantment::INFINITY);
|
||||
}
|
||||
let has_infinity = weapon
|
||||
.get_data_component::<pumpkin_data::data_component_impl::EnchantmentsImpl>()
|
||||
.is_some_and(|enchantments| {
|
||||
enchantments
|
||||
.enchantment
|
||||
.iter()
|
||||
.any(|(e, _)| **e == pumpkin_data::Enchantment::INFINITY)
|
||||
});
|
||||
|
||||
Self::fire_arrow(player, power, &projectile);
|
||||
let is_crit = (power - 1.0).abs() < f32::EPSILON;
|
||||
Self::shoot(
|
||||
player,
|
||||
weapon,
|
||||
std::slice::from_ref(&projectile),
|
||||
power,
|
||||
1.0,
|
||||
is_crit,
|
||||
);
|
||||
|
||||
// Consume arrow (if not creative and no Infinity)
|
||||
if let Some(slot) = arrow_slot
|
||||
@ -133,70 +139,58 @@ impl BowItem {
|
||||
power
|
||||
}
|
||||
|
||||
/// Fire an arrow from the bow
|
||||
pub fn fire_arrow(player: &Player, power: f32, projectile: &ItemStack) {
|
||||
if power < 0.1 {
|
||||
return; // Not enough charge
|
||||
/// Creates projectile matching vanilla `ProjectileWeaponItem::createProjectile`.
|
||||
pub fn create_projectile(
|
||||
player: &Player,
|
||||
weapon: &ItemStack,
|
||||
projectile: &ItemStack,
|
||||
is_crit: bool,
|
||||
) -> ArrowEntity {
|
||||
let world = player.world();
|
||||
let is_creative = player.gamemode.load() == GameMode::Creative;
|
||||
ProjectileWeaponItem::create_projectile(
|
||||
world,
|
||||
player.get_entity(),
|
||||
weapon,
|
||||
projectile,
|
||||
is_crit,
|
||||
is_creative,
|
||||
)
|
||||
}
|
||||
|
||||
/// Shoot projectile matching vanilla `ProjectileWeaponItem::shoot`.
|
||||
pub fn shoot(
|
||||
player: &Player,
|
||||
weapon: &ItemStack,
|
||||
projectiles: &[ItemStack],
|
||||
power: f32,
|
||||
uncertainty: f32,
|
||||
is_crit: bool,
|
||||
) {
|
||||
if power < 0.1 || projectiles.is_empty() {
|
||||
return;
|
||||
}
|
||||
|
||||
let world = player.world();
|
||||
let position = player.position();
|
||||
let is_creative = player.gamemode.load() == GameMode::Creative;
|
||||
let speed = power * ProjectileWeaponItem::ARROW_SPEED_MULTIPLIER;
|
||||
|
||||
// Create arrow entity
|
||||
let arrow_entity = Entity::new(
|
||||
world.clone(),
|
||||
position,
|
||||
ArrowEntity::entity_type_for_item(projectile.item),
|
||||
ProjectileWeaponItem::shoot_projectiles(
|
||||
&world,
|
||||
player.get_entity(),
|
||||
weapon,
|
||||
projectiles,
|
||||
speed,
|
||||
uncertainty,
|
||||
is_crit,
|
||||
is_creative,
|
||||
);
|
||||
|
||||
// Determine pickup mode based on gamemode
|
||||
let gamemode = player.gamemode.load();
|
||||
let pickup = if gamemode == GameMode::Creative {
|
||||
ArrowPickup::CreativeOnly
|
||||
} else {
|
||||
ArrowPickup::Allowed
|
||||
};
|
||||
|
||||
let mut arrow =
|
||||
ArrowEntity::new_shot(arrow_entity, player.get_entity(), projectile, pickup);
|
||||
|
||||
// Read enchantments of the held item (bow)
|
||||
let stack = player.inventory().held_item();
|
||||
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 *= 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();
|
||||
let speed = power * Self::ARROW_SPEED_MULTIPLIER;
|
||||
arrow.set_velocity_from_rotation(pitch, yaw, 0.0, speed, 1.0);
|
||||
|
||||
// Set critical if fully charged
|
||||
if power >= 1.0 {
|
||||
arrow.set_critical(true);
|
||||
}
|
||||
|
||||
// Spawn the arrow entity in the world
|
||||
let arrow_arc: Arc<dyn EntityBase> = Arc::new(arrow);
|
||||
world.spawn_entity(arrow_arc);
|
||||
|
||||
// Play bow shoot sound
|
||||
let sound_pitch = 1.0 / (rand::random::<f32>() * 0.4 + 1.2) + power * 0.5;
|
||||
let sound_packet = CSoundEffect::new(
|
||||
IdOr::Id(Sound::EntityArrowShoot as u16),
|
||||
SoundCategory::Neutral,
|
||||
&position,
|
||||
&player.position(),
|
||||
1.0,
|
||||
sound_pitch,
|
||||
0.0,
|
||||
@ -204,4 +198,27 @@ impl BowItem {
|
||||
let chunk_pos = player.get_entity().chunk_pos.load();
|
||||
world.broadcast_to_chunk(chunk_pos, &sound_packet);
|
||||
}
|
||||
|
||||
/// Fire an arrow from the bow with explicit critical flag
|
||||
pub fn fire_arrow_with_crit(
|
||||
player: &Player,
|
||||
power: f32,
|
||||
projectile: &ItemStack,
|
||||
is_crit: bool,
|
||||
) {
|
||||
let held = player.inventory().held_item();
|
||||
Self::shoot(
|
||||
player,
|
||||
&held,
|
||||
std::slice::from_ref(projectile),
|
||||
power,
|
||||
1.0,
|
||||
is_crit,
|
||||
);
|
||||
}
|
||||
|
||||
/// Fire an arrow from the bow
|
||||
pub fn fire_arrow(player: &Player, power: f32, projectile: &ItemStack) {
|
||||
Self::fire_arrow_with_crit(player, power, projectile, power >= 1.0);
|
||||
}
|
||||
}
|
||||
|
||||
@ -156,17 +156,17 @@ pub(crate) fn try_pickup_fluid_at(
|
||||
|
||||
if is_waterlogged(block, state) {
|
||||
let state_id = set_waterlogged(block, state, false);
|
||||
world.set_block_state(&block_pos, state_id, BlockFlags::NOTIFY_NEIGHBORS);
|
||||
world.set_block_state(&block_pos, state_id, BlockFlags::NOTIFY_ALL);
|
||||
world.schedule_fluid_tick(&Fluid::WATER, block_pos, 5, TickPriority::Normal);
|
||||
return Some(&Item::WATER_BUCKET);
|
||||
}
|
||||
|
||||
if state == Block::LAVA.default_state.id || state == Block::WATER.default_state.id {
|
||||
world.break_block(&block_pos, None, BlockFlags::NOTIFY_NEIGHBORS);
|
||||
world.break_block(&block_pos, None, BlockFlags::NOTIFY_ALL);
|
||||
world.set_block_state(
|
||||
&block_pos,
|
||||
Block::AIR.default_state.id,
|
||||
BlockFlags::NOTIFY_NEIGHBORS,
|
||||
BlockFlags::NOTIFY_ALL,
|
||||
);
|
||||
return Some(if state == Block::LAVA.default_state.id {
|
||||
&Item::LAVA_BUCKET
|
||||
@ -191,7 +191,7 @@ fn try_pickup_bucket_item(
|
||||
let (block, state) = world.get_block_and_state_id(&target_pos);
|
||||
if waterlogged_check(block, state).is_some() {
|
||||
let state_id = set_waterlogged(block, state, false);
|
||||
world.set_block_state(&target_pos, state_id, BlockFlags::NOTIFY_NEIGHBORS);
|
||||
world.set_block_state(&target_pos, state_id, BlockFlags::NOTIFY_ALL);
|
||||
world.schedule_fluid_tick(&Fluid::WATER, target_pos, 5, TickPriority::Normal);
|
||||
return Some(&Item::WATER_BUCKET);
|
||||
}
|
||||
@ -247,7 +247,7 @@ pub(crate) fn try_place_filled_bucket(
|
||||
|
||||
if is_waterlogged(block, state.id) && item.id == Item::WATER_BUCKET.id {
|
||||
let state_id = set_waterlogged(block, state.id, true);
|
||||
world.set_block_state(&pos, state_id, BlockFlags::NOTIFY_NEIGHBORS);
|
||||
world.set_block_state(&pos, state_id, BlockFlags::NOTIFY_ALL);
|
||||
world.schedule_fluid_tick(&Fluid::WATER, pos, 5, TickPriority::Normal);
|
||||
return true;
|
||||
}
|
||||
@ -260,7 +260,7 @@ pub(crate) fn try_place_filled_bucket(
|
||||
return false;
|
||||
}
|
||||
let state_id = set_waterlogged(block, state.id, true);
|
||||
world.set_block_state(&target_pos, state_id, BlockFlags::NOTIFY_NEIGHBORS);
|
||||
world.set_block_state(&target_pos, state_id, BlockFlags::NOTIFY_ALL);
|
||||
world.schedule_fluid_tick(&Fluid::WATER, target_pos, 5, TickPriority::Normal);
|
||||
return true;
|
||||
}
|
||||
@ -273,7 +273,7 @@ pub(crate) fn try_place_filled_bucket(
|
||||
} else {
|
||||
Block::WATER.default_state.id
|
||||
},
|
||||
BlockFlags::NOTIFY_NEIGHBORS,
|
||||
BlockFlags::NOTIFY_ALL,
|
||||
);
|
||||
return true;
|
||||
}
|
||||
|
||||
@ -1,13 +1,12 @@
|
||||
use std::any::Any;
|
||||
use std::sync::Arc;
|
||||
use std::sync::atomic::Ordering;
|
||||
|
||||
use crate::entity::EntityBase;
|
||||
use crate::entity::player::Player;
|
||||
use crate::entity::projectile::arrow::{ArrowEntity, ArrowPickup};
|
||||
use crate::entity::{Entity, EntityBase};
|
||||
use crate::item::items::projectile_weapon::ProjectileWeaponItem;
|
||||
use crate::item::{ItemBehaviour, ItemMetadata};
|
||||
use pumpkin_data::data_component::DataComponent;
|
||||
use pumpkin_data::data_component_impl::{ChargedProjectilesImpl, EnchantmentsImpl};
|
||||
use pumpkin_data::data_component_impl::ChargedProjectilesImpl;
|
||||
use pumpkin_data::item::Item;
|
||||
use pumpkin_data::item_stack::ItemStack;
|
||||
use pumpkin_data::sound::{Sound, SoundCategory};
|
||||
@ -51,61 +50,53 @@ impl ItemBehaviour for CrossbowItem {
|
||||
let use_ticks = player.living_entity.item_use_time.load(Ordering::Relaxed);
|
||||
let use_ticks = 72000 - use_ticks;
|
||||
|
||||
let mut charge_time = 25;
|
||||
let mut stack = player.inventory().held_item();
|
||||
|
||||
if let Some(enchantments) = stack.get_data_component::<EnchantmentsImpl>() {
|
||||
for (enchantment, level) in enchantments.enchantment.iter() {
|
||||
if **enchantment == pumpkin_data::Enchantment::QUICK_CHARGE {
|
||||
charge_time -= 5 * level;
|
||||
}
|
||||
}
|
||||
}
|
||||
charge_time = charge_time.max(0);
|
||||
let charge_time =
|
||||
crate::enchantment::EnchantmentHelper::modify_crossbow_charge_time(&stack, 25);
|
||||
|
||||
if use_ticks >= charge_time {
|
||||
let arrow_slot = player.find_arrow();
|
||||
let (arrow_nbt_wrapper, slot) = arrow_slot.map_or_else(
|
||||
|| {
|
||||
if player.gamemode.load() == GameMode::Creative {
|
||||
let mut arrow_nbt = pumpkin_nbt::compound::NbtCompound::new();
|
||||
let arrow_stack = ItemStack::new(1, &Item::ARROW);
|
||||
arrow_stack.write_item_stack(&mut arrow_nbt);
|
||||
let gamemode = player.gamemode.load();
|
||||
let is_creative = gamemode == GameMode::Creative;
|
||||
|
||||
(Some(arrow_nbt), 0)
|
||||
} else {
|
||||
(None, 0)
|
||||
}
|
||||
},
|
||||
|slot| {
|
||||
let inventory = player.inventory();
|
||||
|
||||
let arrow_stack = inventory.get_slot(slot);
|
||||
let mut arrow_nbt = pumpkin_nbt::compound::NbtCompound::new();
|
||||
arrow_stack
|
||||
.copy_with_count(1)
|
||||
.write_item_stack(&mut arrow_nbt);
|
||||
(Some(arrow_nbt), slot)
|
||||
},
|
||||
);
|
||||
if let Some(arrow_nbt) = arrow_nbt_wrapper {
|
||||
stack.patch.push((
|
||||
DataComponent::ChargedProjectiles,
|
||||
Some(Box::new(ChargedProjectilesImpl {
|
||||
projectiles: vec![arrow_nbt],
|
||||
})),
|
||||
));
|
||||
player.inventory().set_held_item(stack);
|
||||
|
||||
if player.gamemode.load() != GameMode::Creative {
|
||||
player.consume_arrow(slot);
|
||||
}
|
||||
|
||||
player.world().play_sound(
|
||||
Sound::ItemCrossbowLoadingEnd,
|
||||
SoundCategory::Players,
|
||||
&player.position(),
|
||||
if arrow_slot.is_some() || is_creative {
|
||||
let projectile = arrow_slot.map_or_else(
|
||||
|| ItemStack::new(1, &Item::ARROW),
|
||||
|slot| {
|
||||
let inventory = player.inventory();
|
||||
inventory.get_slot(slot).copy_with_count(1)
|
||||
},
|
||||
);
|
||||
|
||||
let drawn = ProjectileWeaponItem::draw(&stack, &projectile, is_creative);
|
||||
if !drawn.is_empty() {
|
||||
let mut charged_nbts = Vec::new();
|
||||
for item in drawn {
|
||||
let mut arrow_nbt = pumpkin_nbt::compound::NbtCompound::new();
|
||||
item.write_item_stack(&mut arrow_nbt);
|
||||
charged_nbts.push(arrow_nbt);
|
||||
}
|
||||
|
||||
stack.patch.push((
|
||||
DataComponent::ChargedProjectiles,
|
||||
Some(Box::new(ChargedProjectilesImpl {
|
||||
projectiles: charged_nbts,
|
||||
})),
|
||||
));
|
||||
player.inventory().set_held_item(stack);
|
||||
|
||||
if let Some(slot) = arrow_slot
|
||||
&& !is_creative
|
||||
{
|
||||
player.consume_arrow(slot);
|
||||
}
|
||||
|
||||
player.world().play_sound(
|
||||
Sound::ItemCrossbowLoadingEnd,
|
||||
SoundCategory::Players,
|
||||
&player.position(),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
player.living_entity.clear_active_hand();
|
||||
@ -121,66 +112,45 @@ impl ItemBehaviour for CrossbowItem {
|
||||
}
|
||||
|
||||
impl CrossbowItem {
|
||||
pub const ARROW_POWER: f32 = 3.15;
|
||||
|
||||
fn fire_projectiles(player: &Player) {
|
||||
let mut held = player.inventory().held_item();
|
||||
let projectiles = held.get_data_component::<ChargedProjectilesImpl>().cloned();
|
||||
let has_multishot =
|
||||
held.get_data_component::<EnchantmentsImpl>()
|
||||
.is_some_and(|enchantments| {
|
||||
enchantments
|
||||
.enchantment
|
||||
.iter()
|
||||
.any(|(e, _)| **e == pumpkin_data::Enchantment::MULTISHOT)
|
||||
});
|
||||
|
||||
if let Some(charged) = projectiles {
|
||||
let world = player.world();
|
||||
world.play_sound(
|
||||
Sound::ItemCrossbowShoot,
|
||||
SoundCategory::Players,
|
||||
&player.position(),
|
||||
);
|
||||
|
||||
let (yaw, pitch) = player.rotation();
|
||||
let charged_opt = held.get_data_component::<ChargedProjectilesImpl>().cloned();
|
||||
|
||||
if let Some(charged) = charged_opt {
|
||||
let mut projectiles = Vec::new();
|
||||
for projectile_nbt in charged.projectiles {
|
||||
let Some(projectile) = ItemStack::read_item_stack(&projectile_nbt) else {
|
||||
continue;
|
||||
};
|
||||
let yaws = if has_multishot {
|
||||
vec![yaw - 10.0, yaw, yaw + 10.0]
|
||||
} else {
|
||||
vec![yaw]
|
||||
};
|
||||
|
||||
for t_yaw in yaws {
|
||||
let arrow_entity = Entity::new(
|
||||
world.clone(),
|
||||
player.position(),
|
||||
ArrowEntity::entity_type_for_item(projectile.item),
|
||||
);
|
||||
let pickup = if player.gamemode.load() == GameMode::Creative {
|
||||
ArrowPickup::CreativeOnly
|
||||
} else {
|
||||
ArrowPickup::Allowed
|
||||
};
|
||||
|
||||
let arrow = ArrowEntity::new_shot(
|
||||
arrow_entity,
|
||||
player.get_entity(),
|
||||
&projectile,
|
||||
pickup,
|
||||
);
|
||||
arrow.set_velocity_from_rotation(pitch, t_yaw, 0.0, 3.15, 1.0);
|
||||
let arrow_arc: Arc<dyn EntityBase> = Arc::new(arrow);
|
||||
world.spawn_entity(arrow_arc);
|
||||
if let Some(projectile) = ItemStack::read_item_stack(&projectile_nbt) {
|
||||
projectiles.push(projectile);
|
||||
}
|
||||
}
|
||||
|
||||
held.patch
|
||||
.retain(|(id, _)| *id != DataComponent::ChargedProjectiles);
|
||||
player.inventory().set_held_item(held);
|
||||
player.damage_held_item(1);
|
||||
if !projectiles.is_empty() {
|
||||
let world = player.world();
|
||||
world.play_sound(
|
||||
Sound::ItemCrossbowShoot,
|
||||
SoundCategory::Players,
|
||||
&player.position(),
|
||||
);
|
||||
|
||||
let is_creative = player.gamemode.load() == GameMode::Creative;
|
||||
ProjectileWeaponItem::shoot_projectiles(
|
||||
&world,
|
||||
player.get_entity(),
|
||||
&held,
|
||||
&projectiles,
|
||||
Self::ARROW_POWER,
|
||||
1.0,
|
||||
false,
|
||||
is_creative,
|
||||
);
|
||||
|
||||
held.patch
|
||||
.retain(|(id, _)| *id != DataComponent::ChargedProjectiles);
|
||||
player.inventory().set_held_item(held);
|
||||
player.damage_held_item(1);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -7,11 +7,13 @@ use pumpkin_data::{Block, BlockStateId};
|
||||
use pumpkin_util::math::position::BlockPos;
|
||||
use pumpkin_util::math::vector3::Vector3;
|
||||
use pumpkin_world::world::BlockFlags;
|
||||
use rand::{RngExt, rng};
|
||||
use std::sync::Arc;
|
||||
|
||||
use crate::entity::player::Player;
|
||||
use crate::item::items::ignite::ignition::Ignition;
|
||||
use crate::item::{ItemBehaviour, ItemMetadata};
|
||||
use crate::plugin::api::events::world::portal_create::{PortalCreateEvent, PortalType};
|
||||
use crate::server::Server;
|
||||
use crate::world::World;
|
||||
|
||||
@ -26,7 +28,7 @@ impl ItemMetadata for FireChargeItem {
|
||||
impl ItemBehaviour for FireChargeItem {
|
||||
fn use_on_block(
|
||||
&self,
|
||||
_item: &mut ItemStack,
|
||||
item: &mut ItemStack,
|
||||
player: &Player,
|
||||
location: BlockPos,
|
||||
face: BlockDirection,
|
||||
@ -35,17 +37,48 @@ impl ItemBehaviour for FireChargeItem {
|
||||
_server: &Server,
|
||||
) {
|
||||
let world = player.world();
|
||||
Ignition::ignite_block(
|
||||
let server_ref = world.server.upgrade();
|
||||
if let Some(server_ref) = server_ref {
|
||||
let player_arc = world.get_player_by_uuid(player.gameprofile.id);
|
||||
let mut event = crate::plugin::api::events::block::block_ignite::BlockIgniteEvent::new(
|
||||
location,
|
||||
&pumpkin_data::Block::FIRE,
|
||||
player_arc,
|
||||
);
|
||||
server_ref
|
||||
.plugin_manager
|
||||
.fire_blocking(&server_ref, &mut event);
|
||||
let mut portal_event = PortalCreateEvent::new(location, PortalType::Nether);
|
||||
server_ref
|
||||
.plugin_manager
|
||||
.fire_blocking(&server_ref, &mut portal_event);
|
||||
if event.cancelled || portal_event.cancelled {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
let ignited = Ignition::ignite_block(
|
||||
|world: Arc<World>, pos: BlockPos, new_state_id: BlockStateId| {
|
||||
world.set_block_state(&pos, new_state_id, BlockFlags::NOTIFY_ALL);
|
||||
|
||||
world.play_block_sound(Sound::ItemFirechargeUse, SoundCategory::Blocks, pos);
|
||||
let pitch = (rng().random::<f32>() - rng().random::<f32>()).mul_add(0.2, 1.0);
|
||||
world.play_sound_fine(
|
||||
Sound::ItemFirechargeUse,
|
||||
SoundCategory::Blocks,
|
||||
&pos.to_centered_f64(),
|
||||
1.0,
|
||||
pitch,
|
||||
);
|
||||
},
|
||||
&world,
|
||||
location,
|
||||
location.offset(face.to_offset()),
|
||||
block,
|
||||
);
|
||||
|
||||
if ignited && player.gamemode.load() != pumpkin_util::GameMode::Creative {
|
||||
item.decrement(1);
|
||||
}
|
||||
}
|
||||
|
||||
fn as_any(&self) -> &dyn std::any::Any {
|
||||
|
||||
@ -32,6 +32,7 @@ pub mod minecart;
|
||||
pub mod name_tag;
|
||||
pub mod on_a_stick;
|
||||
pub mod potions;
|
||||
pub mod projectile_weapon;
|
||||
pub mod saddle;
|
||||
pub mod shears;
|
||||
pub mod shovel;
|
||||
|
||||
172
crates/pumpkin/src/item/items/projectile_weapon.rs
Normal file
172
crates/pumpkin/src/item/items/projectile_weapon.rs
Normal file
@ -0,0 +1,172 @@
|
||||
use std::sync::Arc;
|
||||
use std::sync::atomic::Ordering;
|
||||
|
||||
use crate::enchantment::EnchantmentHelper;
|
||||
use crate::entity::projectile::arrow::{ArrowEntity, ArrowPickup};
|
||||
use crate::entity::{Entity, EntityBase};
|
||||
use crate::world::World;
|
||||
use pumpkin_data::item::Item;
|
||||
use pumpkin_data::item_stack::ItemStack;
|
||||
|
||||
/// Abstraction for projectile-firing weapons (Bow, Crossbow), matching vanilla `ProjectileWeaponItem`.
|
||||
pub struct ProjectileWeaponItem;
|
||||
|
||||
impl ProjectileWeaponItem {
|
||||
pub const ARROW_SPEED_MULTIPLIER: f32 = 3.0;
|
||||
|
||||
#[must_use]
|
||||
pub const fn is_arrow(item: &Item) -> bool {
|
||||
item.id == Item::ARROW.id
|
||||
|| item.id == Item::TIPPED_ARROW.id
|
||||
|| item.id == Item::SPECTRAL_ARROW.id
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub const fn is_arrow_or_firework(item: &Item) -> bool {
|
||||
Self::is_arrow(item) || item.id == Item::FIREWORK_ROCKET.id
|
||||
}
|
||||
|
||||
/// Creates a projectile matching vanilla `ProjectileWeaponItem::createProjectile`.
|
||||
pub fn create_projectile(
|
||||
world: Arc<World>,
|
||||
shooter: &Entity,
|
||||
weapon: &ItemStack,
|
||||
projectile: &ItemStack,
|
||||
is_crit: bool,
|
||||
is_creative: bool,
|
||||
) -> ArrowEntity {
|
||||
let arrow_entity = Entity::new(
|
||||
world,
|
||||
shooter.pos.load(),
|
||||
ArrowEntity::entity_type_for_item(projectile.item),
|
||||
);
|
||||
let pickup = if is_creative {
|
||||
ArrowPickup::CreativeOnly
|
||||
} else {
|
||||
ArrowPickup::Allowed
|
||||
};
|
||||
let arrow =
|
||||
ArrowEntity::new_shot_with_weapon(arrow_entity, shooter, projectile, weapon, pickup);
|
||||
|
||||
// Apply data-driven weapon enchantment modifications
|
||||
arrow.set_base_damage(EnchantmentHelper::modify_damage(
|
||||
weapon,
|
||||
arrow.get_base_damage(),
|
||||
));
|
||||
arrow.punch_level.store(
|
||||
EnchantmentHelper::modify_knockback(weapon, 0.0) as u8,
|
||||
Ordering::Relaxed,
|
||||
);
|
||||
arrow.set_pierce_level(EnchantmentHelper::process_projectile_piercing(weapon, 0));
|
||||
|
||||
// Data-driven PROJECTILE_SPAWNED effects (e.g. Flame enchantment -> Ignite effect)
|
||||
arrow.apply_on_projectile_spawned(projectile);
|
||||
|
||||
if is_crit {
|
||||
arrow.set_critical(true);
|
||||
}
|
||||
|
||||
arrow
|
||||
}
|
||||
|
||||
/// Draws ammo for projectile weapon matching vanilla `ProjectileWeaponItem::draw`.
|
||||
#[must_use]
|
||||
pub fn draw(weapon: &ItemStack, projectile: &ItemStack, is_creative: bool) -> Vec<ItemStack> {
|
||||
if projectile.item_count == 0 {
|
||||
return Vec::new();
|
||||
}
|
||||
|
||||
let num_projectiles = EnchantmentHelper::process_projectile_count(weapon, 1);
|
||||
let mut drawn = Vec::with_capacity(num_projectiles);
|
||||
let projectile_copy = projectile.copy_with_count(1);
|
||||
|
||||
for i in 0..num_projectiles {
|
||||
let drawn_stack = Self::use_ammo(
|
||||
weapon,
|
||||
if i == 0 { projectile } else { &projectile_copy },
|
||||
is_creative,
|
||||
i > 0,
|
||||
);
|
||||
if drawn_stack.item_count > 0 {
|
||||
drawn.push(drawn_stack);
|
||||
}
|
||||
}
|
||||
|
||||
drawn
|
||||
}
|
||||
|
||||
/// Handles ammo usage logic matching vanilla `ProjectileWeaponItem::useAmmo`.
|
||||
#[must_use]
|
||||
pub fn use_ammo(
|
||||
weapon: &ItemStack,
|
||||
projectile: &ItemStack,
|
||||
is_creative: bool,
|
||||
force_infinite: bool,
|
||||
) -> ItemStack {
|
||||
let ammo_to_use = if force_infinite || is_creative {
|
||||
0
|
||||
} else {
|
||||
EnchantmentHelper::process_ammo_use(weapon, projectile, 1)
|
||||
};
|
||||
|
||||
if ammo_to_use > projectile.item_count as i32 {
|
||||
return ItemStack::EMPTY.clone();
|
||||
}
|
||||
|
||||
let mut copy = projectile.copy_with_count(1);
|
||||
if ammo_to_use == 0 {
|
||||
copy.set_data_component(pumpkin_data::data_component_impl::IntangibleProjectileImpl);
|
||||
}
|
||||
|
||||
copy
|
||||
}
|
||||
|
||||
/// Shoots projectiles matching vanilla `ProjectileWeaponItem::shoot`.
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
pub fn shoot_projectiles(
|
||||
world: &Arc<World>,
|
||||
shooter: &Entity,
|
||||
weapon: &ItemStack,
|
||||
projectiles: &[ItemStack],
|
||||
power: f32,
|
||||
uncertainty: f32,
|
||||
is_crit: bool,
|
||||
is_creative: bool,
|
||||
) {
|
||||
if projectiles.is_empty() {
|
||||
return;
|
||||
}
|
||||
|
||||
let max_angle = EnchantmentHelper::process_projectile_spread(weapon, 0.0);
|
||||
let angle_step = if projectiles.len() == 1 {
|
||||
0.0
|
||||
} else {
|
||||
2.0 * max_angle / (projectiles.len() as f32 - 1.0)
|
||||
};
|
||||
let angle_offset = ((projectiles.len() - 1) % 2) as f32 * angle_step / 2.0;
|
||||
let mut direction = 1.0f32;
|
||||
|
||||
let (yaw, pitch) = (shooter.yaw.load(), shooter.pitch.load());
|
||||
|
||||
for (i, projectile) in projectiles.iter().enumerate() {
|
||||
if projectile.item_count == 0 {
|
||||
continue;
|
||||
}
|
||||
|
||||
let angle = angle_offset + direction * (i.div_ceil(2) as f32) * angle_step;
|
||||
direction = -direction;
|
||||
|
||||
let arrow = Self::create_projectile(
|
||||
world.clone(),
|
||||
shooter,
|
||||
weapon,
|
||||
projectile,
|
||||
is_crit,
|
||||
is_creative,
|
||||
);
|
||||
arrow.set_velocity_from_rotation(pitch, yaw + angle, 0.0, power, uncertainty);
|
||||
let arrow_arc: Arc<dyn EntityBase> = Arc::new(arrow);
|
||||
world.spawn_entity(arrow_arc);
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -95,10 +95,11 @@ impl ItemBehaviour for TridentItem {
|
||||
let trident = TridentEntity::new_shot(
|
||||
entity,
|
||||
player.get_entity(),
|
||||
stack_guard,
|
||||
stack_guard.clone(),
|
||||
ArrowPickup::Allowed,
|
||||
);
|
||||
trident.set_velocity_from_rotation(pitch, yaw, 0.0, 2.5, 1.0);
|
||||
trident.apply_on_projectile_spawned(&stack_guard);
|
||||
world.spawn_entity(Arc::new(trident));
|
||||
|
||||
world.play_sound(
|
||||
|
||||
@ -52,6 +52,7 @@ pub mod block;
|
||||
pub mod command;
|
||||
pub mod crash;
|
||||
pub mod data;
|
||||
pub mod enchantment;
|
||||
pub mod entity;
|
||||
pub mod error;
|
||||
pub mod item;
|
||||
|
||||
@ -44,7 +44,7 @@ impl JavaClient {
|
||||
player.world().set_block_state(
|
||||
&command.pos,
|
||||
new_state_id,
|
||||
BlockFlags::SKIP_BLOCK_ADDED_CALLBACK,
|
||||
BlockFlags::NOTIFY_ALL | BlockFlags::SKIP_BLOCK_ADDED_CALLBACK,
|
||||
);
|
||||
|
||||
let mut cmd = command.command;
|
||||
|
||||
@ -134,7 +134,7 @@ pub fn build() -> TokenStream {
|
||||
quote! {
|
||||
use crate::tag::{RegistryKey, Tag, Taggable};
|
||||
|
||||
#[derive(Clone, Copy, PartialEq)]
|
||||
#[derive(Clone, Copy, PartialEq, Debug)]
|
||||
pub struct DamageType {
|
||||
pub death_message_type: DeathMessageType,
|
||||
pub exhaustion: f32,
|
||||
@ -144,14 +144,14 @@ pub fn build() -> TokenStream {
|
||||
pub id: u8,
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, PartialEq)]
|
||||
#[derive(Clone, Copy, PartialEq, Debug)]
|
||||
pub enum DeathMessageType {
|
||||
Default,
|
||||
FallVariants,
|
||||
IntentionalGameDesign,
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, PartialEq)]
|
||||
#[derive(Clone, Copy, PartialEq, Debug)]
|
||||
pub enum DamageEffects {
|
||||
Hurt,
|
||||
Thorns,
|
||||
@ -161,7 +161,7 @@ pub fn build() -> TokenStream {
|
||||
Freezing,
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, PartialEq)]
|
||||
#[derive(Clone, Copy, PartialEq, Debug)]
|
||||
pub enum DamageScaling {
|
||||
Never,
|
||||
WhenCausedByLivingNonPlayer,
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
use std::{collections::BTreeMap, fs};
|
||||
|
||||
use heck::ToShoutySnakeCase;
|
||||
use heck::{ToPascalCase, ToShoutySnakeCase};
|
||||
use proc_macro2::TokenStream;
|
||||
use pumpkin_util::text::TextComponent;
|
||||
use pumpkin_util::text::TextContent::Translate;
|
||||
@ -24,13 +24,16 @@ pub struct Enchantment {
|
||||
/// Maximum level this enchantment can reach.
|
||||
pub max_level: i32,
|
||||
/// Equipment slots this enchantment's attribute modifiers apply to.
|
||||
pub slots: Vec<AttributeModifierSlot>, // TODO: add more
|
||||
pub slots: Vec<AttributeModifierSlot>,
|
||||
/// The weight of this enchantment (used for random selection).
|
||||
pub weight: i32,
|
||||
/// The minimum cost to get this enchantment.
|
||||
pub min_cost: Cost,
|
||||
/// The maximum cost to get this enchantment.
|
||||
pub max_cost: Cost,
|
||||
/// Data-driven effects map.
|
||||
#[serde(default)]
|
||||
pub effects: serde_json::Map<String, serde_json::Value>,
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Clone, Copy)]
|
||||
@ -43,27 +46,16 @@ pub struct Cost {
|
||||
#[derive(Deserialize, Clone)]
|
||||
#[serde(rename_all = "lowercase")]
|
||||
pub enum AttributeModifierSlot {
|
||||
/// Applies in any equipment slot.
|
||||
Any,
|
||||
/// Applies only when held in the main hand.
|
||||
MainHand,
|
||||
/// Applies only when held in the offhand.
|
||||
OffHand,
|
||||
/// Applies when held in either hand.
|
||||
Hand,
|
||||
/// Applies when worn on the feet.
|
||||
Feet,
|
||||
/// Applies when worn on the legs.
|
||||
Legs,
|
||||
/// Applies when worn on the chest.
|
||||
Chest,
|
||||
/// Applies when worn on the head.
|
||||
Head,
|
||||
/// Applies when worn on armor.
|
||||
Armor,
|
||||
/// Applies when worn on body.
|
||||
Body,
|
||||
/// Applies when equipped as saddle.
|
||||
Saddle,
|
||||
}
|
||||
|
||||
@ -86,8 +78,417 @@ impl AttributeModifierSlot {
|
||||
}
|
||||
}
|
||||
|
||||
fn parse_level_based_value(val: &serde_json::Value) -> TokenStream {
|
||||
if let Some(num) = val.as_f64() {
|
||||
let num_f32 = num as f32;
|
||||
return quote! { LevelBasedValue::Constant(#num_f32) };
|
||||
}
|
||||
|
||||
if let Some(obj) = val.as_object() {
|
||||
let val_type = obj.get("type").and_then(|t| t.as_str()).unwrap_or("");
|
||||
match val_type {
|
||||
"minecraft:linear" => {
|
||||
let base = obj.get("base").and_then(|b| b.as_f64()).unwrap_or(0.0) as f32;
|
||||
let per_level = obj
|
||||
.get("per_level_above_first")
|
||||
.and_then(|p| p.as_f64())
|
||||
.unwrap_or(0.0) as f32;
|
||||
quote! {
|
||||
LevelBasedValue::Linear {
|
||||
base: #base,
|
||||
per_level_above_first: #per_level,
|
||||
}
|
||||
}
|
||||
}
|
||||
"minecraft:clamped" => {
|
||||
let inner =
|
||||
parse_level_based_value(obj.get("value").unwrap_or(&serde_json::Value::Null));
|
||||
let min = obj.get("min").and_then(|m| m.as_f64()).unwrap_or(0.0) as f32;
|
||||
let max = obj.get("max").and_then(|m| m.as_f64()).unwrap_or(0.0) as f32;
|
||||
quote! {
|
||||
LevelBasedValue::Clamped {
|
||||
value: &#inner,
|
||||
min: #min,
|
||||
max: #max,
|
||||
}
|
||||
}
|
||||
}
|
||||
"minecraft:fraction" => {
|
||||
let num = parse_level_based_value(
|
||||
obj.get("numerator").unwrap_or(&serde_json::Value::Null),
|
||||
);
|
||||
let denom = parse_level_based_value(
|
||||
obj.get("denominator").unwrap_or(&serde_json::Value::Null),
|
||||
);
|
||||
quote! {
|
||||
LevelBasedValue::Fraction {
|
||||
numerator: &#num,
|
||||
denominator: &#denom,
|
||||
}
|
||||
}
|
||||
}
|
||||
"minecraft:levels_squared" => {
|
||||
let added = obj.get("added").and_then(|a| a.as_f64()).unwrap_or(0.0) as f32;
|
||||
quote! {
|
||||
LevelBasedValue::LevelsSquared { added: #added }
|
||||
}
|
||||
}
|
||||
"minecraft:lookup" => {
|
||||
let values: Vec<f32> = obj
|
||||
.get("values")
|
||||
.and_then(|v| v.as_array())
|
||||
.map(|arr| {
|
||||
arr.iter()
|
||||
.filter_map(|x| x.as_f64().map(|f| f as f32))
|
||||
.collect()
|
||||
})
|
||||
.unwrap_or_default();
|
||||
let fallback = parse_level_based_value(
|
||||
obj.get("fallback").unwrap_or(&serde_json::Value::Null),
|
||||
);
|
||||
quote! {
|
||||
LevelBasedValue::Lookup {
|
||||
values: &[#(#values),*],
|
||||
fallback: &#fallback,
|
||||
}
|
||||
}
|
||||
}
|
||||
_ => quote! { LevelBasedValue::Constant(0.0) },
|
||||
}
|
||||
} else {
|
||||
quote! { LevelBasedValue::Constant(0.0) }
|
||||
}
|
||||
}
|
||||
|
||||
fn parse_entity_effect(val: &serde_json::Value) -> TokenStream {
|
||||
if let Some(obj) = val.as_object() {
|
||||
let eff_type = obj.get("type").and_then(|t| t.as_str()).unwrap_or("");
|
||||
match eff_type {
|
||||
"minecraft:ignite" => {
|
||||
let dur = parse_level_based_value(
|
||||
obj.get("duration").unwrap_or(&serde_json::Value::Null),
|
||||
);
|
||||
quote! { EnchantmentEntityEffect::Ignite { duration: #dur } }
|
||||
}
|
||||
"minecraft:damage_entity" => {
|
||||
let dmg = parse_level_based_value(
|
||||
obj.get("damage")
|
||||
.or_else(|| obj.get("min_damage"))
|
||||
.unwrap_or(&serde_json::Value::Null),
|
||||
);
|
||||
quote! { EnchantmentEntityEffect::DamageEntity { damage: #dmg } }
|
||||
}
|
||||
"minecraft:play_sound" => {
|
||||
let sound = obj.get("sound").and_then(|s| s.as_str()).unwrap_or("");
|
||||
quote! { EnchantmentEntityEffect::PlaySound { sound: #sound } }
|
||||
}
|
||||
"minecraft:replace_block" => {
|
||||
let offset_x = obj
|
||||
.get("offset")
|
||||
.and_then(|v| v.get(0))
|
||||
.and_then(|n| n.as_i64())
|
||||
.unwrap_or(0) as i32;
|
||||
let offset_y = obj
|
||||
.get("offset")
|
||||
.and_then(|v| v.get(1))
|
||||
.and_then(|n| n.as_i64())
|
||||
.unwrap_or(0) as i32;
|
||||
let offset_z = obj
|
||||
.get("offset")
|
||||
.and_then(|v| v.get(2))
|
||||
.and_then(|n| n.as_i64())
|
||||
.unwrap_or(0) as i32;
|
||||
let event = obj.get("trigger_game_event").and_then(|s| s.as_str());
|
||||
let event_token = match event {
|
||||
Some(ev) => {
|
||||
let ev_raw = ev.strip_prefix("minecraft:").unwrap_or(ev);
|
||||
let ev_ident = format_ident!("{}", ev_raw.to_pascal_case());
|
||||
quote! { Some(crate::game_event::GameEvent::#ev_ident) }
|
||||
}
|
||||
None => quote! { None },
|
||||
};
|
||||
quote! {
|
||||
EnchantmentEntityEffect::ReplaceBlock {
|
||||
offset_x: #offset_x,
|
||||
offset_y: #offset_y,
|
||||
offset_z: #offset_z,
|
||||
trigger_game_event: #event_token,
|
||||
}
|
||||
}
|
||||
}
|
||||
"minecraft:explode" => {
|
||||
let attribute_to_user = obj
|
||||
.get("attribute_to_user")
|
||||
.and_then(|b| b.as_bool())
|
||||
.unwrap_or(false);
|
||||
let damage_type = obj.get("damage_type").and_then(|s| s.as_str());
|
||||
let damage_type_token = match damage_type {
|
||||
Some(dt) => {
|
||||
let dt_raw = dt.strip_prefix("minecraft:").unwrap_or(dt);
|
||||
let dt_ident = format_ident!("{}", dt_raw.to_shouty_snake_case());
|
||||
quote! { Some(&crate::damage::DamageType::#dt_ident) }
|
||||
}
|
||||
None => quote! { None },
|
||||
};
|
||||
let knockback_multiplier =
|
||||
obj.get("knockback_multiplier").map(parse_level_based_value);
|
||||
let knockback_token = match knockback_multiplier {
|
||||
Some(kb) => quote! { Some(#kb) },
|
||||
None => quote! { None },
|
||||
};
|
||||
let immune_blocks = obj.get("immune_blocks").and_then(|s| s.as_str());
|
||||
let immune_blocks_token = match immune_blocks {
|
||||
Some(ib) => quote! { Some(#ib) },
|
||||
None => quote! { None },
|
||||
};
|
||||
let offset_x = obj
|
||||
.get("offset")
|
||||
.and_then(|v| v.get(0))
|
||||
.and_then(|n| n.as_f64())
|
||||
.unwrap_or(0.0);
|
||||
let offset_y = obj
|
||||
.get("offset")
|
||||
.and_then(|v| v.get(1))
|
||||
.and_then(|n| n.as_f64())
|
||||
.unwrap_or(0.0);
|
||||
let offset_z = obj
|
||||
.get("offset")
|
||||
.and_then(|v| v.get(2))
|
||||
.and_then(|n| n.as_f64())
|
||||
.unwrap_or(0.0);
|
||||
let radius =
|
||||
parse_level_based_value(obj.get("radius").unwrap_or(&serde_json::Value::Null));
|
||||
let create_fire = obj
|
||||
.get("create_fire")
|
||||
.and_then(|b| b.as_bool())
|
||||
.unwrap_or(false);
|
||||
let block_interaction = obj
|
||||
.get("block_interaction")
|
||||
.and_then(|s| s.as_str())
|
||||
.unwrap_or("none");
|
||||
let small_particle = obj.get("small_particle").and_then(|p| {
|
||||
p.get("type")
|
||||
.and_then(|t| t.as_str())
|
||||
.or_else(|| p.as_str())
|
||||
});
|
||||
let small_particle_token = match small_particle {
|
||||
Some(p) => {
|
||||
let p_raw = p.strip_prefix("minecraft:").unwrap_or(p);
|
||||
let p_ident = format_ident!("{}", p_raw.to_pascal_case());
|
||||
quote! { Some(crate::particle::Particle::#p_ident) }
|
||||
}
|
||||
None => quote! { None },
|
||||
};
|
||||
let large_particle = obj.get("large_particle").and_then(|p| {
|
||||
p.get("type")
|
||||
.and_then(|t| t.as_str())
|
||||
.or_else(|| p.as_str())
|
||||
});
|
||||
let large_particle_token = match large_particle {
|
||||
Some(p) => {
|
||||
let p_raw = p.strip_prefix("minecraft:").unwrap_or(p);
|
||||
let p_ident = format_ident!("{}", p_raw.to_pascal_case());
|
||||
quote! { Some(crate::particle::Particle::#p_ident) }
|
||||
}
|
||||
None => quote! { None },
|
||||
};
|
||||
let sound = obj.get("sound").and_then(|s| s.as_str());
|
||||
let sound_token = match sound {
|
||||
Some(s) => {
|
||||
let s_raw = s.strip_prefix("minecraft:").unwrap_or(s);
|
||||
let s_ident = format_ident!("{}", s_raw.to_pascal_case());
|
||||
quote! { Some(crate::sound::Sound::#s_ident) }
|
||||
}
|
||||
None => quote! { None },
|
||||
};
|
||||
|
||||
quote! {
|
||||
EnchantmentEntityEffect::Explode {
|
||||
attribute_to_user: #attribute_to_user,
|
||||
damage_type: #damage_type_token,
|
||||
knockback_multiplier: #knockback_token,
|
||||
immune_blocks: #immune_blocks_token,
|
||||
offset_x: #offset_x,
|
||||
offset_y: #offset_y,
|
||||
offset_z: #offset_z,
|
||||
radius: #radius,
|
||||
create_fire: #create_fire,
|
||||
block_interaction: #block_interaction,
|
||||
small_particle: #small_particle_token,
|
||||
large_particle: #large_particle_token,
|
||||
sound: #sound_token,
|
||||
}
|
||||
}
|
||||
}
|
||||
"minecraft:all_of" => {
|
||||
let inner: Vec<TokenStream> = obj
|
||||
.get("effects")
|
||||
.and_then(|arr| arr.as_array())
|
||||
.map(|arr| arr.iter().map(parse_entity_effect).collect())
|
||||
.unwrap_or_default();
|
||||
quote! { EnchantmentEntityEffect::AllOf(&[#(#inner),*]) }
|
||||
}
|
||||
_ => quote! { EnchantmentEntityEffect::Other },
|
||||
}
|
||||
} else {
|
||||
quote! { EnchantmentEntityEffect::Other }
|
||||
}
|
||||
}
|
||||
|
||||
fn parse_value_effect(val: &serde_json::Value) -> TokenStream {
|
||||
if let Some(obj) = val.as_object() {
|
||||
let eff_type = obj.get("type").and_then(|t| t.as_str()).unwrap_or("");
|
||||
match eff_type {
|
||||
"minecraft:add" => {
|
||||
let v =
|
||||
parse_level_based_value(obj.get("value").unwrap_or(&serde_json::Value::Null));
|
||||
quote! { EnchantmentValueEffect::Add(#v) }
|
||||
}
|
||||
"minecraft:multiply" => {
|
||||
let v = parse_level_based_value(
|
||||
obj.get("factor")
|
||||
.or_else(|| obj.get("value"))
|
||||
.unwrap_or(&serde_json::Value::Null),
|
||||
);
|
||||
quote! { EnchantmentValueEffect::Multiply(#v) }
|
||||
}
|
||||
"minecraft:set" => {
|
||||
let v =
|
||||
parse_level_based_value(obj.get("value").unwrap_or(&serde_json::Value::Null));
|
||||
quote! { EnchantmentValueEffect::Set(#v) }
|
||||
}
|
||||
"minecraft:remove_binomial" => {
|
||||
let v =
|
||||
parse_level_based_value(obj.get("chance").unwrap_or(&serde_json::Value::Null));
|
||||
quote! { EnchantmentValueEffect::RemoveBinomial(#v) }
|
||||
}
|
||||
_ => quote! { EnchantmentValueEffect::Other },
|
||||
}
|
||||
} else {
|
||||
quote! { EnchantmentValueEffect::Other }
|
||||
}
|
||||
}
|
||||
|
||||
fn parse_conditional_entity_effects(val: Option<&serde_json::Value>) -> TokenStream {
|
||||
let effects: Vec<TokenStream> = val
|
||||
.and_then(|v| v.as_array())
|
||||
.map(|arr| {
|
||||
arr.iter()
|
||||
.map(|item| {
|
||||
let eff = item.get("effect").unwrap_or(item);
|
||||
let eff_tokens = parse_entity_effect(eff);
|
||||
quote! { ConditionalEffect { effect: #eff_tokens } }
|
||||
})
|
||||
.collect()
|
||||
})
|
||||
.unwrap_or_default();
|
||||
quote! { &[#(#effects),*] }
|
||||
}
|
||||
|
||||
fn parse_targeted_conditional_entity_effects(val: Option<&serde_json::Value>) -> TokenStream {
|
||||
let effects: Vec<TokenStream> = val
|
||||
.and_then(|v| v.as_array())
|
||||
.map(|arr| {
|
||||
arr.iter()
|
||||
.map(|item| {
|
||||
let eff = item.get("effect").unwrap_or(item);
|
||||
let eff_tokens = parse_entity_effect(eff);
|
||||
let enchanted = match item.get("enchanted").and_then(|e| e.as_str()) {
|
||||
Some("attacker") => quote! { Some(EnchantmentTarget::Attacker) },
|
||||
Some("damaging_entity") => {
|
||||
quote! { Some(EnchantmentTarget::DamagingEntity) }
|
||||
}
|
||||
Some("victim") => quote! { Some(EnchantmentTarget::Victim) },
|
||||
_ => quote! { None },
|
||||
};
|
||||
let affected = match item.get("affected").and_then(|e| e.as_str()) {
|
||||
Some("attacker") => quote! { Some(EnchantmentTarget::Attacker) },
|
||||
Some("damaging_entity") => {
|
||||
quote! { Some(EnchantmentTarget::DamagingEntity) }
|
||||
}
|
||||
Some("victim") => quote! { Some(EnchantmentTarget::Victim) },
|
||||
_ => quote! { None },
|
||||
};
|
||||
quote! {
|
||||
TargetedConditionalEffect {
|
||||
enchanted: #enchanted,
|
||||
affected: #affected,
|
||||
effect: #eff_tokens,
|
||||
}
|
||||
}
|
||||
})
|
||||
.collect()
|
||||
})
|
||||
.unwrap_or_default();
|
||||
quote! { &[#(#effects),*] }
|
||||
}
|
||||
|
||||
fn parse_conditional_value_effects(val: Option<&serde_json::Value>) -> TokenStream {
|
||||
let effects: Vec<TokenStream> = val
|
||||
.and_then(|v| v.as_array())
|
||||
.map(|arr| {
|
||||
arr.iter()
|
||||
.map(|item| {
|
||||
let eff = item.get("effect").unwrap_or(item);
|
||||
let eff_tokens = parse_value_effect(eff);
|
||||
quote! { ConditionalEffect { effect: #eff_tokens } }
|
||||
})
|
||||
.collect()
|
||||
})
|
||||
.unwrap_or_default();
|
||||
quote! { &[#(#effects),*] }
|
||||
}
|
||||
|
||||
fn parse_targeted_conditional_value_effects(val: Option<&serde_json::Value>) -> TokenStream {
|
||||
let effects: Vec<TokenStream> = val
|
||||
.and_then(|v| v.as_array())
|
||||
.map(|arr| {
|
||||
arr.iter()
|
||||
.map(|item| {
|
||||
let eff = item.get("effect").unwrap_or(item);
|
||||
let eff_tokens = parse_value_effect(eff);
|
||||
let enchanted = match item.get("enchanted").and_then(|e| e.as_str()) {
|
||||
Some("attacker") => quote! { Some(EnchantmentTarget::Attacker) },
|
||||
Some("damaging_entity") => {
|
||||
quote! { Some(EnchantmentTarget::DamagingEntity) }
|
||||
}
|
||||
Some("victim") => quote! { Some(EnchantmentTarget::Victim) },
|
||||
_ => quote! { None },
|
||||
};
|
||||
let affected = match item.get("affected").and_then(|e| e.as_str()) {
|
||||
Some("attacker") => quote! { Some(EnchantmentTarget::Attacker) },
|
||||
Some("damaging_entity") => {
|
||||
quote! { Some(EnchantmentTarget::DamagingEntity) }
|
||||
}
|
||||
Some("victim") => quote! { Some(EnchantmentTarget::Victim) },
|
||||
_ => quote! { None },
|
||||
};
|
||||
quote! {
|
||||
TargetedConditionalEffect {
|
||||
enchanted: #enchanted,
|
||||
affected: #affected,
|
||||
effect: #eff_tokens,
|
||||
}
|
||||
}
|
||||
})
|
||||
.collect()
|
||||
})
|
||||
.unwrap_or_default();
|
||||
quote! { &[#(#effects),*] }
|
||||
}
|
||||
|
||||
fn parse_optional_value_effect(val: Option<&serde_json::Value>) -> TokenStream {
|
||||
if let Some(v) = val {
|
||||
let eff = v.get("effect").unwrap_or(v);
|
||||
let eff_tokens = parse_value_effect(eff);
|
||||
quote! { Some(#eff_tokens) }
|
||||
} else {
|
||||
quote! { None }
|
||||
}
|
||||
}
|
||||
|
||||
/// Generates the `TokenStream` for the `Enchantment` struct, `AttributeModifierSlot` enum,
|
||||
/// and `from_name`/`from_id` lookup methods.
|
||||
/// data-driven effect types, and `from_name`/`from_id` lookup methods.
|
||||
pub fn build() -> TokenStream {
|
||||
let dir = std::path::Path::new("../../assets/datapacks/26_2/data/minecraft/enchantment");
|
||||
let mut enchantments: BTreeMap<String, Enchantment> = BTreeMap::new();
|
||||
@ -152,6 +553,80 @@ pub fn build() -> TokenStream {
|
||||
};
|
||||
let translate = translate.to_string();
|
||||
|
||||
let effects_map = enchantment.effects;
|
||||
let projectile_spawned =
|
||||
parse_conditional_entity_effects(effects_map.get("minecraft:projectile_spawned"));
|
||||
let post_attack =
|
||||
parse_targeted_conditional_entity_effects(effects_map.get("minecraft:post_attack"));
|
||||
let projectile_count =
|
||||
parse_conditional_value_effects(effects_map.get("minecraft:projectile_count"));
|
||||
let projectile_spread =
|
||||
parse_conditional_value_effects(effects_map.get("minecraft:projectile_spread"));
|
||||
let projectile_piercing =
|
||||
parse_conditional_value_effects(effects_map.get("minecraft:projectile_piercing"));
|
||||
let ammo_use = parse_conditional_value_effects(effects_map.get("minecraft:ammo_use"));
|
||||
let damage = parse_conditional_value_effects(effects_map.get("minecraft:damage"));
|
||||
let knockback = parse_conditional_value_effects(effects_map.get("minecraft:knockback"));
|
||||
let armor_effectiveness =
|
||||
parse_conditional_value_effects(effects_map.get("minecraft:armor_effectiveness"));
|
||||
let damage_protection =
|
||||
parse_conditional_value_effects(effects_map.get("minecraft:damage_protection"));
|
||||
let hit_block = parse_conditional_entity_effects(effects_map.get("minecraft:hit_block"));
|
||||
let item_damage = parse_conditional_value_effects(effects_map.get("minecraft:item_damage"));
|
||||
let equipment_drops =
|
||||
parse_targeted_conditional_value_effects(effects_map.get("minecraft:equipment_drops"));
|
||||
let fishing_time_reduction =
|
||||
parse_conditional_value_effects(effects_map.get("minecraft:fishing_time_reduction"));
|
||||
let fishing_luck_bonus =
|
||||
parse_conditional_value_effects(effects_map.get("minecraft:fishing_luck_bonus"));
|
||||
let block_experience =
|
||||
parse_conditional_value_effects(effects_map.get("minecraft:block_experience"));
|
||||
let mob_experience =
|
||||
parse_conditional_value_effects(effects_map.get("minecraft:mob_experience"));
|
||||
let repair_with_xp =
|
||||
parse_conditional_value_effects(effects_map.get("minecraft:repair_with_xp"));
|
||||
let smash_damage_per_fallen_block = parse_conditional_value_effects(
|
||||
effects_map.get("minecraft:smash_damage_per_fallen_block"),
|
||||
);
|
||||
let trident_return_acceleration = parse_conditional_value_effects(
|
||||
effects_map.get("minecraft:trident_return_acceleration"),
|
||||
);
|
||||
let trident_spin_attack_strength =
|
||||
parse_optional_value_effect(effects_map.get("minecraft:trident_spin_attack_strength"));
|
||||
let crossbow_charge_time =
|
||||
parse_optional_value_effect(effects_map.get("minecraft:crossbow_charge_time"));
|
||||
let prevent_armor_change = effects_map.contains_key("minecraft:prevent_armor_change");
|
||||
let prevent_equipment_drop = effects_map.contains_key("minecraft:prevent_equipment_drop");
|
||||
|
||||
let effects_tokens = quote! {
|
||||
EnchantmentEffects {
|
||||
projectile_spawned: #projectile_spawned,
|
||||
post_attack: #post_attack,
|
||||
projectile_count: #projectile_count,
|
||||
projectile_spread: #projectile_spread,
|
||||
projectile_piercing: #projectile_piercing,
|
||||
ammo_use: #ammo_use,
|
||||
damage: #damage,
|
||||
knockback: #knockback,
|
||||
armor_effectiveness: #armor_effectiveness,
|
||||
damage_protection: #damage_protection,
|
||||
hit_block: #hit_block,
|
||||
item_damage: #item_damage,
|
||||
equipment_drops: #equipment_drops,
|
||||
fishing_time_reduction: #fishing_time_reduction,
|
||||
fishing_luck_bonus: #fishing_luck_bonus,
|
||||
block_experience: #block_experience,
|
||||
mob_experience: #mob_experience,
|
||||
repair_with_xp: #repair_with_xp,
|
||||
smash_damage_per_fallen_block: #smash_damage_per_fallen_block,
|
||||
trident_return_acceleration: #trident_return_acceleration,
|
||||
trident_spin_attack_strength: #trident_spin_attack_strength,
|
||||
crossbow_charge_time: #crossbow_charge_time,
|
||||
prevent_armor_change: #prevent_armor_change,
|
||||
prevent_equipment_drop: #prevent_equipment_drop,
|
||||
}
|
||||
};
|
||||
|
||||
if let Some(exclusive_set) = &enchantment.exclusive_set {
|
||||
let exclusive_set = format_ident!(
|
||||
"{}",
|
||||
@ -181,6 +656,7 @@ pub fn build() -> TokenStream {
|
||||
base: #max_cost_base,
|
||||
per_level_above_first: #max_cost_per_level,
|
||||
},
|
||||
effects: #effects_tokens,
|
||||
};
|
||||
}]);
|
||||
} else {
|
||||
@ -204,6 +680,7 @@ pub fn build() -> TokenStream {
|
||||
base: #max_cost_base,
|
||||
per_level_above_first: #max_cost_per_level,
|
||||
},
|
||||
effects: #effects_tokens,
|
||||
};
|
||||
}]);
|
||||
}
|
||||
@ -223,6 +700,176 @@ pub fn build() -> TokenStream {
|
||||
use std::hash::{Hash, Hasher};
|
||||
use std::slice::Iter;
|
||||
|
||||
#[derive(Clone, Debug, PartialEq)]
|
||||
pub enum LevelBasedValue {
|
||||
Constant(f32),
|
||||
Linear {
|
||||
base: f32,
|
||||
per_level_above_first: f32,
|
||||
},
|
||||
Clamped {
|
||||
value: &'static Self,
|
||||
min: f32,
|
||||
max: f32,
|
||||
},
|
||||
Fraction {
|
||||
numerator: &'static Self,
|
||||
denominator: &'static Self,
|
||||
},
|
||||
LevelsSquared {
|
||||
added: f32,
|
||||
},
|
||||
Lookup {
|
||||
values: &'static [f32],
|
||||
fallback: &'static Self,
|
||||
},
|
||||
}
|
||||
|
||||
impl LevelBasedValue {
|
||||
#[must_use]
|
||||
pub fn calculate(&self, level: i32) -> f32 {
|
||||
match self {
|
||||
Self::Constant(val) => *val,
|
||||
Self::Linear {
|
||||
base,
|
||||
per_level_above_first,
|
||||
} => base + (level.max(1) - 1) as f32 * per_level_above_first,
|
||||
Self::Clamped { value, min, max } => value.calculate(level).clamp(*min, *max),
|
||||
Self::Fraction {
|
||||
numerator,
|
||||
denominator,
|
||||
} => {
|
||||
let denom = denominator.calculate(level);
|
||||
if denom == 0.0 {
|
||||
0.0
|
||||
} else {
|
||||
numerator.calculate(level) / denom
|
||||
}
|
||||
}
|
||||
Self::LevelsSquared { added } => ((level * level) as f32) + added,
|
||||
Self::Lookup { values, fallback } => {
|
||||
let idx = (level - 1) as usize;
|
||||
values
|
||||
.get(idx)
|
||||
.copied()
|
||||
.unwrap_or_else(|| fallback.calculate(level))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
|
||||
pub enum EnchantmentTarget {
|
||||
Attacker,
|
||||
DamagingEntity,
|
||||
Victim,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||
pub struct TargetedConditionalEffect<T> {
|
||||
pub enchanted: Option<EnchantmentTarget>,
|
||||
pub affected: Option<EnchantmentTarget>,
|
||||
pub effect: T,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||
pub struct ConditionalEffect<T> {
|
||||
pub effect: T,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq)]
|
||||
pub enum EnchantmentEntityEffect {
|
||||
Ignite {
|
||||
duration: LevelBasedValue,
|
||||
},
|
||||
DamageEntity {
|
||||
damage: LevelBasedValue,
|
||||
},
|
||||
PlaySound {
|
||||
sound: &'static str,
|
||||
},
|
||||
ReplaceBlock {
|
||||
offset_x: i32,
|
||||
offset_y: i32,
|
||||
offset_z: i32,
|
||||
trigger_game_event: Option<crate::game_event::GameEvent>,
|
||||
},
|
||||
Explode {
|
||||
attribute_to_user: bool,
|
||||
damage_type: Option<&'static crate::damage::DamageType>,
|
||||
knockback_multiplier: Option<LevelBasedValue>,
|
||||
immune_blocks: Option<&'static str>,
|
||||
offset_x: f64,
|
||||
offset_y: f64,
|
||||
offset_z: f64,
|
||||
radius: LevelBasedValue,
|
||||
create_fire: bool,
|
||||
block_interaction: &'static str,
|
||||
small_particle: Option<crate::particle::Particle>,
|
||||
large_particle: Option<crate::particle::Particle>,
|
||||
sound: Option<crate::sound::Sound>,
|
||||
},
|
||||
AllOf(&'static [EnchantmentEntityEffect]),
|
||||
Other,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq)]
|
||||
pub enum EnchantmentValueEffect {
|
||||
Add(LevelBasedValue),
|
||||
Multiply(LevelBasedValue),
|
||||
Set(LevelBasedValue),
|
||||
RemoveBinomial(LevelBasedValue),
|
||||
Other,
|
||||
}
|
||||
|
||||
impl EnchantmentValueEffect {
|
||||
#[must_use]
|
||||
pub fn process(&self, level: i32, current_value: f32) -> f32 {
|
||||
match self {
|
||||
Self::Add(val) => current_value + val.calculate(level),
|
||||
Self::Multiply(val) => current_value * val.calculate(level),
|
||||
Self::Set(val) => val.calculate(level),
|
||||
Self::RemoveBinomial(val) => {
|
||||
let prob = val.calculate(level);
|
||||
if rand::random::<f32>() < prob {
|
||||
0.0
|
||||
} else {
|
||||
current_value
|
||||
}
|
||||
}
|
||||
Self::Other => current_value,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct EnchantmentEffects {
|
||||
pub projectile_spawned: &'static [ConditionalEffect<EnchantmentEntityEffect>],
|
||||
pub post_attack: &'static [TargetedConditionalEffect<EnchantmentEntityEffect>],
|
||||
pub projectile_count: &'static [ConditionalEffect<EnchantmentValueEffect>],
|
||||
pub projectile_spread: &'static [ConditionalEffect<EnchantmentValueEffect>],
|
||||
pub projectile_piercing: &'static [ConditionalEffect<EnchantmentValueEffect>],
|
||||
pub ammo_use: &'static [ConditionalEffect<EnchantmentValueEffect>],
|
||||
pub damage: &'static [ConditionalEffect<EnchantmentValueEffect>],
|
||||
pub knockback: &'static [ConditionalEffect<EnchantmentValueEffect>],
|
||||
pub armor_effectiveness: &'static [ConditionalEffect<EnchantmentValueEffect>],
|
||||
pub damage_protection: &'static [ConditionalEffect<EnchantmentValueEffect>],
|
||||
pub hit_block: &'static [ConditionalEffect<EnchantmentEntityEffect>],
|
||||
pub item_damage: &'static [ConditionalEffect<EnchantmentValueEffect>],
|
||||
pub equipment_drops: &'static [TargetedConditionalEffect<EnchantmentValueEffect>],
|
||||
pub fishing_time_reduction: &'static [ConditionalEffect<EnchantmentValueEffect>],
|
||||
pub fishing_luck_bonus: &'static [ConditionalEffect<EnchantmentValueEffect>],
|
||||
pub block_experience: &'static [ConditionalEffect<EnchantmentValueEffect>],
|
||||
pub mob_experience: &'static [ConditionalEffect<EnchantmentValueEffect>],
|
||||
pub repair_with_xp: &'static [ConditionalEffect<EnchantmentValueEffect>],
|
||||
pub smash_damage_per_fallen_block: &'static [ConditionalEffect<EnchantmentValueEffect>],
|
||||
pub trident_return_acceleration: &'static [ConditionalEffect<EnchantmentValueEffect>],
|
||||
pub trident_spin_attack_strength: Option<EnchantmentValueEffect>,
|
||||
pub crossbow_charge_time: Option<EnchantmentValueEffect>,
|
||||
pub prevent_armor_change: bool,
|
||||
pub prevent_equipment_drop: bool,
|
||||
}
|
||||
|
||||
pub struct Enchantment {
|
||||
pub id: u8,
|
||||
pub name: &'static str,
|
||||
@ -236,7 +883,7 @@ pub fn build() -> TokenStream {
|
||||
pub weight: i32,
|
||||
pub min_cost: Cost,
|
||||
pub max_cost: Cost,
|
||||
// TODO: add more
|
||||
pub effects: EnchantmentEffects,
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug)]
|
||||
@ -350,6 +997,118 @@ pub fn build() -> TokenStream {
|
||||
}
|
||||
ret
|
||||
}
|
||||
|
||||
pub fn modify_damage_protection(&self, level: i32, protection: &mut f32) {
|
||||
for effect in self.effects.damage_protection {
|
||||
*protection = effect.effect.process(level, *protection);
|
||||
}
|
||||
}
|
||||
|
||||
pub fn modify_damage(&self, level: i32, amount: &mut f64) {
|
||||
for effect in self.effects.damage {
|
||||
*amount = f64::from(effect.effect.process(level, *amount as f32));
|
||||
}
|
||||
}
|
||||
|
||||
pub fn modify_fall_based_damage(&self, level: i32, amount: &mut f64) {
|
||||
for effect in self.effects.smash_damage_per_fallen_block {
|
||||
*amount = f64::from(effect.effect.process(level, *amount as f32));
|
||||
}
|
||||
}
|
||||
|
||||
pub fn modify_knockback(&self, level: i32, amount: &mut f32) {
|
||||
for effect in self.effects.knockback {
|
||||
*amount = effect.effect.process(level, *amount);
|
||||
}
|
||||
}
|
||||
|
||||
pub fn modify_armor_effectiveness(&self, level: i32, amount: &mut f32) {
|
||||
for effect in self.effects.armor_effectiveness {
|
||||
*amount = effect.effect.process(level, *amount);
|
||||
}
|
||||
}
|
||||
|
||||
pub fn modify_durability_change(&self, level: i32, change: &mut f32) {
|
||||
for effect in self.effects.item_damage {
|
||||
*change = effect.effect.process(level, *change);
|
||||
}
|
||||
}
|
||||
|
||||
pub fn modify_ammo_count(&self, level: i32, change: &mut f32) {
|
||||
for effect in self.effects.ammo_use {
|
||||
*change = effect.effect.process(level, *change);
|
||||
}
|
||||
}
|
||||
|
||||
pub fn modify_piercing_count(&self, level: i32, count: &mut f32) {
|
||||
for effect in self.effects.projectile_piercing {
|
||||
*count = effect.effect.process(level, *count);
|
||||
}
|
||||
}
|
||||
|
||||
pub fn modify_block_experience(&self, level: i32, count: &mut f32) {
|
||||
for effect in self.effects.block_experience {
|
||||
*count = effect.effect.process(level, *count);
|
||||
}
|
||||
}
|
||||
|
||||
pub fn modify_mob_experience(&self, level: i32, experience: &mut f32) {
|
||||
for effect in self.effects.mob_experience {
|
||||
*experience = effect.effect.process(level, *experience);
|
||||
}
|
||||
}
|
||||
|
||||
pub fn modify_durability_to_repair_from_xp(&self, level: i32, change: &mut f32) {
|
||||
for effect in self.effects.repair_with_xp {
|
||||
*change = effect.effect.process(level, *change);
|
||||
}
|
||||
}
|
||||
|
||||
pub fn modify_trident_return_to_owner_acceleration(&self, level: i32, count: &mut f32) {
|
||||
for effect in self.effects.trident_return_acceleration {
|
||||
*count = effect.effect.process(level, *count);
|
||||
}
|
||||
}
|
||||
|
||||
pub fn modify_trident_spin_attack_strength(&self, level: i32, strength: &mut f32) {
|
||||
if let Some(effect) = &self.effects.trident_spin_attack_strength {
|
||||
*strength = effect.process(level, *strength);
|
||||
}
|
||||
}
|
||||
|
||||
pub fn modify_fishing_time_reduction(&self, level: i32, time_reduction: &mut f32) {
|
||||
for effect in self.effects.fishing_time_reduction {
|
||||
*time_reduction = effect.effect.process(level, *time_reduction);
|
||||
}
|
||||
}
|
||||
|
||||
pub fn modify_fishing_luck_bonus(&self, level: i32, luck: &mut f32) {
|
||||
for effect in self.effects.fishing_luck_bonus {
|
||||
*luck = effect.effect.process(level, *luck);
|
||||
}
|
||||
}
|
||||
|
||||
pub fn modify_projectile_count(&self, level: i32, count: &mut f32) {
|
||||
for effect in self.effects.projectile_count {
|
||||
*count = effect.effect.process(level, *count);
|
||||
}
|
||||
}
|
||||
|
||||
pub fn modify_projectile_spread(&self, level: i32, angle: &mut f32) {
|
||||
for effect in self.effects.projectile_spread {
|
||||
*angle = effect.effect.process(level, *angle);
|
||||
}
|
||||
}
|
||||
|
||||
pub fn modify_crossbow_charge_time(&self, level: i32, time: &mut f32) {
|
||||
if let Some(effect) = &self.effects.crossbow_charge_time {
|
||||
*time = effect.process(level, *time);
|
||||
}
|
||||
}
|
||||
|
||||
pub fn get_projectile_spawned_effects(&self) -> &'static [ConditionalEffect<EnchantmentEntityEffect>] {
|
||||
self.effects.projectile_spawned
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,20 +1,43 @@
|
||||
use std::fs;
|
||||
|
||||
use heck::ToPascalCase;
|
||||
use proc_macro2::TokenStream;
|
||||
use quote::quote;
|
||||
|
||||
use crate::array_to_tokenstream;
|
||||
use quote::{format_ident, quote};
|
||||
|
||||
/// Generates the `TokenStream` for the `GameEvent` enum.
|
||||
pub fn build() -> TokenStream {
|
||||
let game_events: Vec<String> =
|
||||
serde_json::from_str(&fs::read_to_string("../../assets/game_event.json").unwrap())
|
||||
.expect("Failed to parse game_event.json");
|
||||
let variants = array_to_tokenstream(&game_events);
|
||||
|
||||
let mut variants = TokenStream::new();
|
||||
let mut from_name_match = TokenStream::new();
|
||||
|
||||
for event in &game_events {
|
||||
let ident = format_ident!("{}", event.to_pascal_case());
|
||||
let namespaced = format!("minecraft:{event}");
|
||||
variants.extend(quote! {
|
||||
#ident,
|
||||
});
|
||||
from_name_match.extend(quote! {
|
||||
#event | #namespaced => Some(Self::#ident),
|
||||
});
|
||||
}
|
||||
|
||||
quote! {
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
|
||||
pub enum GameEvent {
|
||||
#variants
|
||||
}
|
||||
|
||||
impl GameEvent {
|
||||
#[must_use]
|
||||
pub fn from_name(name: &str) -> Option<Self> {
|
||||
match name {
|
||||
#from_name_match
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -139,19 +139,19 @@ pub struct ItemComponents {
|
||||
#[serde(rename = "minecraft:provides_trim_material")]
|
||||
pub provides_trim_material: Option<serde_json::Value>,
|
||||
#[serde(rename = "minecraft:rarity")]
|
||||
pub rarity: Option<serde_json::Value>,
|
||||
pub rarity: Option<String>,
|
||||
#[serde(rename = "minecraft:recipes")]
|
||||
pub recipes: Option<serde_json::Value>,
|
||||
#[serde(rename = "minecraft:repair_cost")]
|
||||
pub repair_cost: Option<serde_json::Value>,
|
||||
pub repair_cost: Option<i32>,
|
||||
#[serde(rename = "minecraft:repairable")]
|
||||
pub repairable: Option<serde_json::Value>,
|
||||
pub repairable: Option<RepairableComponent>,
|
||||
#[serde(rename = "minecraft:stored_enchantments")]
|
||||
pub stored_enchantments: Option<serde_json::Value>,
|
||||
#[serde(rename = "minecraft:suspicious_stew_effects")]
|
||||
pub suspicious_stew_effects: Option<serde_json::Value>,
|
||||
#[serde(rename = "minecraft:swing_animation")]
|
||||
pub swing_animation: Option<serde_json::Value>,
|
||||
pub swing_animation: Option<SwingAnimationComponent>,
|
||||
#[serde(rename = "minecraft:tooltip_display")]
|
||||
pub tooltip_display: Option<serde_json::Value>,
|
||||
#[serde(rename = "minecraft:use_effects")]
|
||||
@ -162,6 +162,22 @@ pub struct ItemComponents {
|
||||
pub writable_book_content: Option<serde_json::Value>,
|
||||
}
|
||||
|
||||
fn default_swing_animation_type() -> String {
|
||||
"whack".to_string()
|
||||
}
|
||||
|
||||
const fn default_swing_animation_duration() -> i32 {
|
||||
6
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Clone)]
|
||||
pub struct SwingAnimationComponent {
|
||||
#[serde(default = "default_swing_animation_type")]
|
||||
pub r#type: String,
|
||||
#[serde(default = "default_swing_animation_duration")]
|
||||
pub duration: i32,
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
pub struct EnchantableComponent {
|
||||
pub value: i32,
|
||||
@ -845,17 +861,50 @@ impl ToTokens for ItemComponents {
|
||||
if self.provides_trim_material.is_some() {
|
||||
tokens.extend(quote! { (ProvidesTrimMaterial, &ProvidesTrimMaterialImpl), });
|
||||
}
|
||||
if self.rarity.is_some() {
|
||||
tokens.extend(quote! { (Rarity, &RarityImpl), });
|
||||
if let Some(rarity_str) = &self.rarity {
|
||||
let rarity_variant = match rarity_str.as_str() {
|
||||
"uncommon" => quote! { crate::data_component_impl::Rarity::Uncommon },
|
||||
"rare" => quote! { crate::data_component_impl::Rarity::Rare },
|
||||
"epic" => quote! { crate::data_component_impl::Rarity::Epic },
|
||||
_ => quote! { crate::data_component_impl::Rarity::Common },
|
||||
};
|
||||
tokens.extend(quote! { (Rarity, &RarityImpl { rarity: #rarity_variant }), });
|
||||
}
|
||||
if self.recipes.is_some() {
|
||||
tokens.extend(quote! { (Recipes, &RecipesImpl), });
|
||||
}
|
||||
if self.repair_cost.is_some() {
|
||||
tokens.extend(quote! { (RepairCost, &RepairCostImpl), });
|
||||
if let Some(cost) = self.repair_cost {
|
||||
tokens.extend(quote! { (RepairCost, &RepairCostImpl { cost: #cost }), });
|
||||
}
|
||||
if self.repairable.is_some() {
|
||||
tokens.extend(quote! { (Repairable, &RepairableImpl), });
|
||||
if let Some(repairable) = &self.repairable {
|
||||
let mut items_tokens = TokenStream::new();
|
||||
match &repairable.items {
|
||||
StringOrList::String(str) => {
|
||||
if let Some(formatted) = str.strip_prefix('#') {
|
||||
items_tokens.extend(quote! {
|
||||
IDSet::Tag(Cow::Borrowed(#formatted))
|
||||
});
|
||||
} else {
|
||||
let item_name = str.strip_prefix("minecraft:").unwrap_or(str);
|
||||
let ident = format_ident!("{}", item_name.to_shouty_snake_case());
|
||||
items_tokens.extend(quote! {
|
||||
IDSet::IDs(Cow::Borrowed(&[&Self::#ident]))
|
||||
});
|
||||
}
|
||||
}
|
||||
StringOrList::List(items) => {
|
||||
let mut ids = TokenStream::new();
|
||||
for x in items {
|
||||
let item_name = x.strip_prefix("minecraft:").unwrap_or(x);
|
||||
let ident = format_ident!("{}", item_name.to_shouty_snake_case());
|
||||
ids.extend(quote! { &Self::#ident, });
|
||||
}
|
||||
items_tokens.extend(quote! {
|
||||
IDSet::IDs(Cow::Borrowed(&[#ids]))
|
||||
});
|
||||
}
|
||||
}
|
||||
tokens.extend(quote! { (Repairable, &RepairableImpl { items: #items_tokens }), });
|
||||
}
|
||||
if self.stored_enchantments.is_some() {
|
||||
tokens.extend(quote! { (StoredEnchantments, &StoredEnchantmentsImpl { enchantment: Cow::Borrowed(&[]) }), });
|
||||
@ -863,8 +912,23 @@ impl ToTokens for ItemComponents {
|
||||
if self.suspicious_stew_effects.is_some() {
|
||||
tokens.extend(quote! { (SuspiciousStewEffects, &SuspiciousStewEffectsImpl::EMPTY), });
|
||||
}
|
||||
if self.swing_animation.is_some() {
|
||||
tokens.extend(quote! { (SwingAnimation, &SwingAnimationImpl), });
|
||||
if let Some(swing) = &self.swing_animation {
|
||||
let anim_type = match swing.r#type.as_str() {
|
||||
"whack" => quote! { SwingAnimationType::Whack },
|
||||
"stab" => quote! { SwingAnimationType::Stab },
|
||||
"none" => quote! { SwingAnimationType::None },
|
||||
_ => quote! { SwingAnimationType::Whack },
|
||||
};
|
||||
let duration = swing.duration;
|
||||
tokens.extend(quote! {
|
||||
(
|
||||
SwingAnimation,
|
||||
&SwingAnimationImpl {
|
||||
animation_type: #anim_type,
|
||||
duration: #duration,
|
||||
},
|
||||
),
|
||||
});
|
||||
}
|
||||
if self.tooltip_display.is_some() {
|
||||
tokens.extend(quote! { (TooltipDisplay, &TooltipDisplayImpl), });
|
||||
@ -1034,6 +1098,11 @@ pub enum StringOrList {
|
||||
List(Vec<String>),
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Clone)]
|
||||
pub struct RepairableComponent {
|
||||
pub items: StringOrList,
|
||||
}
|
||||
|
||||
/// Deserialized equippable component describing how an item is worn or equipped.
|
||||
#[derive(Deserialize, Clone)]
|
||||
pub struct EquippableComponent {
|
||||
@ -1550,6 +1619,15 @@ pub fn build() -> TokenStream {
|
||||
}
|
||||
}
|
||||
|
||||
impl std::fmt::Debug for Item {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
f.debug_struct("Item")
|
||||
.field("id", &self.id)
|
||||
.field("registry_key", &self.registry_key)
|
||||
.finish()
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
pub enum BedrockItemVersion {
|
||||
Legacy,
|
||||
|
||||
@ -646,10 +646,21 @@ pub(crate) fn build() -> TokenStream {
|
||||
fn registry_key(&self) -> &str;
|
||||
fn registry_id(&self) -> u16;
|
||||
|
||||
#[must_use]
|
||||
fn is_tagged_with(&self, tag: &str) -> Option<bool> {
|
||||
let tag = tag.strip_prefix("#").unwrap_or(tag);
|
||||
let items = get_tag_ids(Self::tag_key(), tag)?;
|
||||
#[must_use]
|
||||
fn is_tagged_with(&self, tag: &str) -> Option<bool> {
|
||||
let tag = tag.strip_prefix('#').unwrap_or(tag);
|
||||
let items = get_tag_ids(Self::tag_key(), tag).or_else(|| {
|
||||
if !tag.contains(':') {
|
||||
let mut full = String::with_capacity(10 + tag.len());
|
||||
full.push_str("minecraft:");
|
||||
full.push_str(tag);
|
||||
get_tag_ids(Self::tag_key(), &full)
|
||||
} else if let Some(stripped) = tag.strip_prefix("minecraft:") {
|
||||
get_tag_ids(Self::tag_key(), stripped)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
})?;
|
||||
Some(items.contains(&self.registry_id()))
|
||||
}
|
||||
|
||||
@ -660,8 +671,19 @@ pub(crate) fn build() -> TokenStream {
|
||||
|
||||
#[must_use]
|
||||
fn get_tag_values(tag: &str) -> Option<&'static [&'static str]> {
|
||||
let tag = tag.strip_prefix("#").unwrap_or(tag);
|
||||
get_tag_values(Self::tag_key(), tag)
|
||||
let tag = tag.strip_prefix('#').unwrap_or(tag);
|
||||
get_tag_values(Self::tag_key(), tag).or_else(|| {
|
||||
if !tag.contains(':') {
|
||||
let mut full = String::with_capacity(10 + tag.len());
|
||||
full.push_str("minecraft:");
|
||||
full.push_str(tag);
|
||||
get_tag_values(Self::tag_key(), &full)
|
||||
} else if let Some(stripped) = tag.strip_prefix("minecraft:") {
|
||||
get_tag_values(Self::tag_key(), stripped)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Loading…
Reference in New Issue
Block a user