Pumpkin/pumpkin-codegen/src/enchantments.rs
2026-05-06 22:29:29 +02:00

329 lines
12 KiB
Rust

use std::{collections::BTreeMap, fs};
use heck::ToShoutySnakeCase;
use proc_macro2::TokenStream;
use pumpkin_util::text::TextComponent;
use pumpkin_util::text::TextContent::Translate;
use quote::{format_ident, quote};
use serde::Deserialize;
/// Raw deserialization shape for a single enchantment entry from `enchantments.json`.
#[derive(Deserialize)]
pub struct Enchantment {
/// Numeric registry ID for this enchantment.
pub id: u8,
/// Anvil repair cost multiplier added when applying this enchantment.
pub anvil_cost: u32,
/// Tag path (prefixed with `#`) of items that support this enchantment.
pub supported_items: String,
/// Display name component for this enchantment (typically a translation key).
pub description: TextComponent,
/// Optional exclusive-set tag; enchantments in the same set are mutually incompatible.
pub exclusive_set: Option<String>,
/// 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
/// 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,
}
#[derive(Deserialize, Clone, Copy)]
pub struct Cost {
pub base: i32,
pub per_level_above_first: i32,
}
/// Equipment slot category that an enchantment's attribute modifier applies to.
#[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 wearing any piece of armor.
Armor,
/// Applies when worn as a body armor piece (e.g. on horses or wolves).
Body,
/// Applies when placed in a saddle slot.
Saddle,
}
impl AttributeModifierSlot {
/// Converts this slot variant into a `TokenStream` for use in generated code.
pub fn to_tokens(&self) -> TokenStream {
match self {
Self::Any => quote! { AttributeModifierSlot::Any },
Self::MainHand => quote! { AttributeModifierSlot::MainHand },
Self::OffHand => quote! { AttributeModifierSlot::OffHand },
Self::Hand => quote! { AttributeModifierSlot::Hand },
Self::Feet => quote! { AttributeModifierSlot::Feet },
Self::Legs => quote! { AttributeModifierSlot::Legs },
Self::Chest => quote! { AttributeModifierSlot::Chest },
Self::Head => quote! { AttributeModifierSlot::Head },
Self::Armor => quote! { AttributeModifierSlot::Armor },
Self::Body => quote! { AttributeModifierSlot::Body },
Self::Saddle => quote! { AttributeModifierSlot::Saddle },
}
}
}
/// Generates the `TokenStream` for the `Enchantment` struct, `AttributeModifierSlot` enum,
/// and `from_name`/`from_id` lookup methods.
pub fn build() -> TokenStream {
let enchantments: BTreeMap<String, Enchantment> =
serde_json::from_str(&fs::read_to_string("../assets/enchantments.json").unwrap())
.expect("Failed to parse enchantments.json");
let mut variants = TokenStream::new();
let mut all_variants = TokenStream::new();
let mut name_to_type = TokenStream::new();
let mut id_to_type = TokenStream::new();
for (name, enchantment) in enchantments {
let id = enchantment.id;
let raw_name = name.strip_prefix("minecraft:").unwrap();
let format_name = format_ident!("{}", raw_name.to_shouty_snake_case());
all_variants.extend(quote! { &Self::#format_name, });
let anvil_cost = enchantment.anvil_cost;
let supported_items = format_ident!(
"{}",
enchantment
.supported_items
.strip_prefix("#")
.unwrap()
.replace([':', '/'], "_")
.to_uppercase()
);
let max_level = enchantment.max_level;
let weight = enchantment.weight;
let min_cost_base = enchantment.min_cost.base;
let min_cost_per_level = enchantment.min_cost.per_level_above_first;
let max_cost_base = enchantment.max_cost.base;
let max_cost_per_level = enchantment.max_cost.per_level_above_first;
let slots = enchantment.slots;
let slots = slots.iter().map(AttributeModifierSlot::to_tokens);
let Translate { translate, bedrock_translate: _, with: _ } = &*enchantment.description.0.content else {
panic!()
};
let translate = translate.to_string();
if let Some(exclusive_set) = &enchantment.exclusive_set {
let exclusive_set = format_ident!(
"{}",
exclusive_set
.strip_prefix("#")
.unwrap()
.replace([':', '/'], "_")
.to_uppercase()
);
variants.extend([quote! {
pub const #format_name: Self = Self {
id: #id,
name: #name,
registry_key: #raw_name,
description: #translate,
anvil_cost: #anvil_cost,
supported_items: &ItemTag::#supported_items,
exclusive_set: Some(&EnchantmentTag::#exclusive_set),
max_level: #max_level,
slots: &[#(#slots),*],
weight: #weight,
min_cost: Cost {
base: #min_cost_base,
per_level_above_first: #min_cost_per_level,
},
max_cost: Cost {
base: #max_cost_base,
per_level_above_first: #max_cost_per_level,
},
};
}]);
} else {
variants.extend([quote! {
pub const #format_name: Self = Self {
id: #id,
name: #name,
description: #translate,
registry_key: #raw_name,
anvil_cost: #anvil_cost,
supported_items: &ItemTag::#supported_items,
exclusive_set: None,
max_level: #max_level,
slots: &[#(#slots),*],
weight: #weight,
min_cost: Cost {
base: #min_cost_base,
per_level_above_first: #min_cost_per_level,
},
max_cost: Cost {
base: #max_cost_base,
per_level_above_first: #max_cost_per_level,
},
};
}]);
}
name_to_type.extend(quote! { #name => Some(&Self::#format_name), });
id_to_type.extend(quote! { #id => Some(&Self::#format_name), });
}
quote! {
use crate::item::Item;
use crate::tag::Enchantment as EnchantmentTag;
use crate::tag::Item as ItemTag;
use crate::tag::{RegistryKey, Tag, Taggable};
use crate::data_component_impl::EnchantmentsImpl;
use pumpkin_util::text::TextComponent;
use pumpkin_util::text::color::NamedColor;
use std::hash::{Hash, Hasher};
use std::slice::Iter;
pub struct Enchantment {
pub id: u8,
pub name: &'static str,
pub registry_key: &'static str,
pub description: &'static str, // TODO use TextComponent
pub anvil_cost: u32,
pub supported_items: &'static Tag,
pub exclusive_set: Option<&'static Tag>,
pub max_level: i32,
pub slots: &'static [AttributeModifierSlot],
pub weight: i32,
pub min_cost: Cost,
pub max_cost: Cost,
// TODO: add more
}
#[derive(Clone, Copy, Debug)]
pub struct Cost {
pub base: i32,
pub per_level_above_first: i32,
}
impl Cost {
pub fn calculate(&self, level: i32) -> i32 {
self.base + self.per_level_above_first * (level - 1)
}
}
impl Taggable for Enchantment {
#[inline]
fn tag_key() -> RegistryKey {
RegistryKey::Enchantment
}
#[inline]
fn registry_key(&self) -> &str {
self.registry_key
}
#[inline]
fn registry_id(&self) -> u16 {
self.id as u16
}
}
impl PartialEq for Enchantment {
fn eq(&self, other: &Self) -> bool {
self.id == other.id
}
}
impl Eq for Enchantment {}
impl Hash for Enchantment {
fn hash<H: Hasher>(&self, state: &mut H) {
self.id.hash(state);
}
}
#[derive(Debug, Clone, Hash, PartialEq)]
pub enum AttributeModifierSlot {
Any,
MainHand,
OffHand,
Hand,
Feet,
Legs,
Chest,
Head,
Armor,
Body,
Saddle,
}
impl Enchantment {
pub const ALL: &'static [&'static Self] = &[#all_variants];
pub fn all() -> Iter<'static, &'static Self> {
Self::ALL.iter()
}
#variants
pub fn from_name(name: &str) -> Option<&'static Self> {
match name {
#name_to_type
_ => None
}
}
pub fn from_id(id: u8) -> Option<&'static Self> {
match id {
#id_to_type
_ => None
}
}
pub fn can_enchant(&self, item: &'static Item) -> bool {
self.supported_items.1.contains(&item.id)
}
pub fn are_compatible(&self, other: &'static Enchantment) -> bool {
if self == other {
return false;
}
if let Some(tag) = self.exclusive_set && tag.1.contains(&(other.id as u16)) {
return false;
}
if let Some(tag) = other.exclusive_set && tag.1.contains(&(self.id as u16)) {
return false;
}
true
}
pub fn is_enchantment_compatible(&self, other: &EnchantmentsImpl) -> bool {
for (i, _) in other.enchantment.iter() {
if !self.are_compatible(i) {
return false;
}
}
true
}
pub fn get_fullname(&self, level: i32) -> TextComponent {
let mut ret = TextComponent::translate(self.description, []).color_named(
if self.has_tag(&EnchantmentTag::MINECRAFT_CURSE) {
NamedColor::Red
} else {
NamedColor::Gray
}
);
if level != 1 || self.max_level != 1 {
ret = ret.add_text(" ")
.add_child(TextComponent::translate("enchantment.level.".to_string() + &level.to_string(), []));
}
ret
}
}
}
}