Pumpkin/crates/pumpkin-inventory/src/enchanting/enchanting_screen_handler.rs

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)));
}
}