486 lines
16 KiB
Rust
486 lines
16 KiB
Rust
use std::any::Any;
|
|
use std::sync::Arc;
|
|
|
|
use pumpkin_data::Enchantment;
|
|
use pumpkin_data::data_component_impl::EnchantableImpl;
|
|
use pumpkin_data::item::Item;
|
|
use pumpkin_data::item_stack::ItemStack;
|
|
use pumpkin_data::screen::WindowType;
|
|
use pumpkin_data::statistic::{CustomStatistic, StatisticCategory};
|
|
use pumpkin_data::tag::{Enchantment as EnchantmentTag, Taggable};
|
|
use pumpkin_util::random::{RandomImpl, legacy_rand::LegacyRand};
|
|
use pumpkin_world::inventory::Inventory;
|
|
|
|
use crate::{
|
|
player::player_inventory::PlayerInventory,
|
|
screen_handler::{
|
|
InventoryPlayer, ItemStackFuture, ScreenHandler, ScreenHandlerBehaviour,
|
|
ScreenHandlerFuture, offer_or_drop_stack,
|
|
},
|
|
slot::{BoxFuture, NormalSlot, Slot},
|
|
window_property::{EnchantmentTable, WindowProperty},
|
|
};
|
|
|
|
struct LapisSlot(NormalSlot);
|
|
|
|
fn is_lapis(stack: &ItemStack) -> bool {
|
|
stack.item == &Item::LAPIS_LAZULI
|
|
}
|
|
|
|
impl LapisSlot {
|
|
fn new(inventory: Arc<dyn Inventory>) -> Self {
|
|
Self(NormalSlot::new(inventory, 1))
|
|
}
|
|
}
|
|
|
|
impl Slot for LapisSlot {
|
|
fn get_inventory(&self) -> Arc<dyn Inventory> {
|
|
self.0.get_inventory()
|
|
}
|
|
|
|
fn get_index(&self) -> usize {
|
|
self.0.get_index()
|
|
}
|
|
|
|
fn set_id(&self, id: usize) {
|
|
self.0.set_id(id);
|
|
}
|
|
|
|
fn can_insert<'a>(&'a self, stack: &'a ItemStack) -> BoxFuture<'a, bool> {
|
|
Box::pin(async move { is_lapis(stack) })
|
|
}
|
|
|
|
fn mark_dirty(&self) -> BoxFuture<'_, ()> {
|
|
self.0.mark_dirty()
|
|
}
|
|
}
|
|
|
|
pub struct EnchantingTableScreenHandler {
|
|
pub inventory: Arc<dyn Inventory>,
|
|
behaviour: ScreenHandlerBehaviour,
|
|
pub level_requirements: [i32; 3],
|
|
pub enchantment_id: [i32; 3],
|
|
pub enchantment_level: [i32; 3],
|
|
pub enchantment_seed: i32,
|
|
pub bookshelf_count: i32,
|
|
}
|
|
|
|
impl EnchantingTableScreenHandler {
|
|
pub fn new(
|
|
sync_id: u8,
|
|
player_inventory: &Arc<PlayerInventory>,
|
|
inventory: &Arc<dyn Inventory>,
|
|
enchantment_seed: i32,
|
|
bookshelf_count: i32,
|
|
) -> Self {
|
|
let mut handler = Self {
|
|
inventory: inventory.clone(),
|
|
behaviour: ScreenHandlerBehaviour::new(sync_id, Some(WindowType::Enchantment)),
|
|
level_requirements: [0; 3],
|
|
enchantment_id: [-1; 3],
|
|
enchantment_level: [-1; 3],
|
|
enchantment_seed,
|
|
bookshelf_count,
|
|
};
|
|
|
|
// Enchanting slots: 0 is item, 1 is lapis
|
|
handler.add_slot(Arc::new(NormalSlot::new(inventory.clone(), 0)));
|
|
handler.add_slot(Arc::new(LapisSlot::new(inventory.clone())));
|
|
|
|
let player_inventory: Arc<dyn Inventory> = player_inventory.clone();
|
|
handler.add_player_slots(&player_inventory);
|
|
|
|
handler
|
|
}
|
|
|
|
pub async fn update_enchantments(&mut self, _player: &dyn InventoryPlayer) {
|
|
let item = self.inventory.get_stack(0).await;
|
|
|
|
if item.is_empty() || item.has_enchantments() {
|
|
for i in 0..3 {
|
|
self.level_requirements[i] = 0;
|
|
self.enchantment_id[i] = -1;
|
|
self.enchantment_level[i] = -1;
|
|
}
|
|
} else {
|
|
let enchantability = item
|
|
.get_data_component::<EnchantableImpl>()
|
|
.map_or(0, |e| e.value);
|
|
|
|
if enchantability <= 0 {
|
|
for i in 0..3 {
|
|
self.level_requirements[i] = 0;
|
|
self.enchantment_id[i] = -1;
|
|
self.enchantment_level[i] = -1;
|
|
}
|
|
} else {
|
|
let mut random = LegacyRand::from_seed(self.enchantment_seed as u64);
|
|
|
|
for i in 0..3 {
|
|
let level = self.calculate_level_requirement(&mut random, i, enchantability);
|
|
self.level_requirements[i] = level;
|
|
}
|
|
|
|
for i in 0..3 {
|
|
if self.level_requirements[i] > 0 {
|
|
let mut random = self.create_enchantment_random(i);
|
|
let enchantments = Self::get_enchantment_list(
|
|
&mut random,
|
|
&item,
|
|
i,
|
|
self.level_requirements[i],
|
|
);
|
|
if enchantments.is_empty() {
|
|
self.enchantment_id[i] = -1;
|
|
self.enchantment_level[i] = -1;
|
|
} else {
|
|
let clue_index =
|
|
random.next_bounded_i32(enchantments.len() as i32) as usize;
|
|
let clue = enchantments[clue_index];
|
|
self.enchantment_id[i] = clue.0.id as i32;
|
|
self.enchantment_level[i] = clue.1;
|
|
}
|
|
} else {
|
|
self.enchantment_id[i] = -1;
|
|
self.enchantment_level[i] = -1;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
self.send_property_updates().await;
|
|
}
|
|
|
|
fn calculate_level_requirement(
|
|
&self,
|
|
random: &mut LegacyRand,
|
|
slot: usize,
|
|
_enchantability: i32,
|
|
) -> i32 {
|
|
let b = self.bookshelf_count;
|
|
let level = random.next_bounded_i32(8) + 1 + (b >> 1) + random.next_bounded_i32(b + 1);
|
|
|
|
match slot {
|
|
0 => (level / 3).max(1),
|
|
1 => (level * 2 / 3 + 7).max(1),
|
|
2 => level.max(b * 2).max(1),
|
|
_ => 0,
|
|
}
|
|
}
|
|
|
|
const fn create_enchantment_random(&self, slot: usize) -> LegacyRand {
|
|
LegacyRand::from_seed(self.enchantment_seed.wrapping_add(slot as i32) as u64)
|
|
}
|
|
|
|
fn get_enchantment_list(
|
|
random: &mut LegacyRand,
|
|
item: &ItemStack,
|
|
_slot: usize,
|
|
level: i32,
|
|
) -> Vec<(&'static Enchantment, i32)> {
|
|
let enchantability = item
|
|
.get_data_component::<EnchantableImpl>()
|
|
.map_or(0, |e| e.value);
|
|
let mut enchant_level = level
|
|
+ 1
|
|
+ random.next_bounded_i32(enchantability / 4 + 1)
|
|
+ random.next_bounded_i32(enchantability / 4 + 1);
|
|
let bonus = (random.next_f32() + random.next_f32() - 1.0) * 0.15;
|
|
enchant_level = (enchant_level as f32 * (1.0 + bonus)).round() as i32;
|
|
enchant_level = enchant_level.max(1);
|
|
|
|
let mut available = Vec::new();
|
|
for enchant in Enchantment::all() {
|
|
if enchant.has_tag(&EnchantmentTag::MINECRAFT_IN_ENCHANTING_TABLE)
|
|
&& enchant.can_enchant(item.item)
|
|
{
|
|
for l in (1..=enchant.max_level).rev() {
|
|
if enchant_level >= enchant.min_cost.calculate(l)
|
|
&& enchant_level <= enchant.max_cost.calculate(l)
|
|
{
|
|
available.push((*enchant, l));
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
if available.is_empty() {
|
|
return Vec::new();
|
|
}
|
|
|
|
let total_weight: i32 = available.iter().map(|(e, _)| e.weight).sum();
|
|
if total_weight <= 0 {
|
|
return Vec::new();
|
|
}
|
|
|
|
let mut weight = random.next_bounded_i32(total_weight);
|
|
let mut selected = None;
|
|
for (e, l) in &available {
|
|
weight -= e.weight;
|
|
if weight < 0 {
|
|
selected = Some((*e, *l));
|
|
break;
|
|
}
|
|
}
|
|
|
|
let mut result = Vec::new();
|
|
if let Some(s) = selected {
|
|
result.push(s);
|
|
|
|
// Add more?
|
|
let mut current_level = enchant_level;
|
|
while random.next_bounded_i32(50) <= (current_level + 1) / 2 {
|
|
available.retain(|(e, _)| {
|
|
for (se, _) in &result {
|
|
if !e.are_compatible(se) {
|
|
return false;
|
|
}
|
|
}
|
|
true
|
|
});
|
|
|
|
if available.is_empty() {
|
|
break;
|
|
}
|
|
|
|
let total_weight: i32 = available.iter().map(|(e, _)| e.weight).sum();
|
|
let mut weight = random.next_bounded_i32(total_weight);
|
|
for (e, l) in &available {
|
|
weight -= e.weight;
|
|
if weight < 0 {
|
|
result.push((*e, *l));
|
|
break;
|
|
}
|
|
}
|
|
current_level /= 2;
|
|
}
|
|
}
|
|
|
|
result
|
|
}
|
|
|
|
async fn send_property_updates(&self) {
|
|
if let Some(sync_handler) = self.behaviour.sync_handler.as_ref() {
|
|
for i in 0..3 {
|
|
let (id, val) = WindowProperty::new(
|
|
EnchantmentTable::LevelRequirement { slot: i as u8 },
|
|
self.level_requirements[i] as i16,
|
|
)
|
|
.into_tuple();
|
|
sync_handler
|
|
.update_property(&self.behaviour, id as i32, val as i32)
|
|
.await;
|
|
|
|
let (id, val) = WindowProperty::new(
|
|
EnchantmentTable::EnchantmentId { slot: i as u8 },
|
|
self.enchantment_id[i] as i16,
|
|
)
|
|
.into_tuple();
|
|
sync_handler
|
|
.update_property(&self.behaviour, id as i32, val as i32)
|
|
.await;
|
|
|
|
let (id, val) = WindowProperty::new(
|
|
EnchantmentTable::EnchantmentLevel { slot: i as u8 },
|
|
self.enchantment_level[i] as i16,
|
|
)
|
|
.into_tuple();
|
|
sync_handler
|
|
.update_property(&self.behaviour, id as i32, val as i32)
|
|
.await;
|
|
}
|
|
|
|
let (id, val) = WindowProperty::new(
|
|
EnchantmentTable::EnchantmentSeed,
|
|
(self.enchantment_seed & 0xFFFF) as i16,
|
|
)
|
|
.into_tuple();
|
|
sync_handler
|
|
.update_property(&self.behaviour, id as i32, val as i32)
|
|
.await;
|
|
}
|
|
}
|
|
}
|
|
|
|
impl ScreenHandler for EnchantingTableScreenHandler {
|
|
fn as_any(&self) -> &dyn Any {
|
|
self
|
|
}
|
|
|
|
fn as_any_mut(&mut self) -> &mut dyn Any {
|
|
self
|
|
}
|
|
|
|
fn get_behaviour(&self) -> &ScreenHandlerBehaviour {
|
|
&self.behaviour
|
|
}
|
|
|
|
fn get_behaviour_mut(&mut self) -> &mut ScreenHandlerBehaviour {
|
|
&mut self.behaviour
|
|
}
|
|
|
|
fn on_closed<'a>(&'a mut self, player: &'a dyn InventoryPlayer) -> ScreenHandlerFuture<'a, ()> {
|
|
Box::pin(async move {
|
|
self.default_on_closed(player).await;
|
|
self.inventory.on_close().await;
|
|
// Return items to player
|
|
for i in 0..2 {
|
|
let stack = self.inventory.remove_stack(i).await;
|
|
if !stack.is_empty() {
|
|
offer_or_drop_stack(player, stack).await;
|
|
}
|
|
}
|
|
})
|
|
}
|
|
|
|
fn on_button_click<'a>(
|
|
&'a mut self,
|
|
player: &'a dyn InventoryPlayer,
|
|
id: i32,
|
|
) -> ScreenHandlerFuture<'a, bool> {
|
|
Box::pin(async move {
|
|
if !(0..3).contains(&id) {
|
|
return false;
|
|
}
|
|
|
|
let level_req = self.level_requirements[id as usize];
|
|
if player.experience_level() < level_req && !player.is_creative() {
|
|
return false;
|
|
}
|
|
|
|
let mut lapis_stack = self.inventory.get_stack(1).await;
|
|
let lapis_cost = (id + 1) as u8;
|
|
|
|
if !player.is_creative()
|
|
&& (lapis_stack.is_empty()
|
|
|| !is_lapis(&lapis_stack)
|
|
|| lapis_stack.item_count < lapis_cost)
|
|
{
|
|
return false;
|
|
}
|
|
|
|
// Perform enchantment
|
|
let mut item_stack = self.inventory.get_stack(0).await;
|
|
|
|
if item_stack.is_empty() || item_stack.has_enchantments() {
|
|
return false;
|
|
}
|
|
|
|
let mut random = self.create_enchantment_random(id as usize);
|
|
let enchantments =
|
|
Self::get_enchantment_list(&mut random, &item_stack, id as usize, level_req);
|
|
|
|
if enchantments.is_empty() {
|
|
return false;
|
|
}
|
|
|
|
if !player.is_creative() {
|
|
player.add_experience_levels(-(id + 1)).await;
|
|
lapis_stack.decrement(lapis_cost);
|
|
self.inventory.set_stack(1, lapis_stack).await;
|
|
}
|
|
|
|
for (enchant, level) in enchantments {
|
|
item_stack.add_enchantment(enchant, level as u16);
|
|
}
|
|
self.inventory.set_stack(0, item_stack).await;
|
|
|
|
// Update seed
|
|
player.set_enchantment_seed(rand::random()).await;
|
|
self.enchantment_seed = player.enchantment_seed();
|
|
|
|
self.update_enchantments(player).await;
|
|
self.send_content_updates().await;
|
|
|
|
player
|
|
.increment_stat(
|
|
StatisticCategory::Custom,
|
|
CustomStatistic::EnchantItem as i32,
|
|
1,
|
|
)
|
|
.await;
|
|
|
|
true
|
|
})
|
|
}
|
|
|
|
fn quick_move<'a>(
|
|
&'a mut self,
|
|
player: &'a dyn InventoryPlayer, // FIX: Changed _player to player
|
|
slot_index: i32,
|
|
) -> ItemStackFuture<'a> {
|
|
Box::pin(async move {
|
|
let mut stack_left = ItemStack::EMPTY.clone();
|
|
let slot = self.get_behaviour().slots[slot_index as usize].clone();
|
|
|
|
if slot.has_stack().await {
|
|
let mut slot_stack = slot.get_stack().await;
|
|
stack_left = slot_stack.clone();
|
|
|
|
if slot_index < 2 {
|
|
// From enchanting to player
|
|
if !self
|
|
.insert_item(
|
|
&mut slot_stack,
|
|
2,
|
|
self.get_behaviour().slots.len() as i32,
|
|
true,
|
|
)
|
|
.await
|
|
{
|
|
return ItemStack::EMPTY.clone();
|
|
}
|
|
} else {
|
|
// From player to enchanting
|
|
// Lapis check
|
|
if slot_stack.item == &Item::LAPIS_LAZULI {
|
|
if !self.insert_item(&mut slot_stack, 1, 2, false).await {
|
|
return ItemStack::EMPTY.clone();
|
|
}
|
|
} else if !self.insert_item(&mut slot_stack, 0, 1, false).await {
|
|
return ItemStack::EMPTY.clone();
|
|
}
|
|
}
|
|
|
|
if slot_stack.is_empty() {
|
|
slot.set_stack(ItemStack::EMPTY.clone()).await;
|
|
} else {
|
|
slot.set_stack(slot_stack).await;
|
|
}
|
|
|
|
// CRITICAL FIX: Ensure the client is notified when shift-clicking items into the slots
|
|
self.update_enchantments(player).await;
|
|
}
|
|
|
|
stack_left
|
|
})
|
|
}
|
|
|
|
fn on_slot_click<'a>(
|
|
&'a mut self,
|
|
slot_index: i32,
|
|
button: i32,
|
|
action_type: pumpkin_protocol::java::server::play::SlotActionType,
|
|
player: &'a dyn InventoryPlayer,
|
|
) -> ScreenHandlerFuture<'a, ()> {
|
|
Box::pin(async move {
|
|
self.internal_on_slot_click(slot_index, button, action_type, player)
|
|
.await;
|
|
if slot_index == 0 || slot_index == 1 {
|
|
self.update_enchantments(player).await;
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use super::*;
|
|
|
|
#[test]
|
|
fn lapis_slot_only_accepts_lapis_lazuli() {
|
|
assert!(is_lapis(&ItemStack::new(1, &Item::LAPIS_LAZULI)));
|
|
assert!(!is_lapis(&ItemStack::new(1, &Item::DIRT)));
|
|
}
|
|
}
|