RyanHub – file viewer
filename: common/src/main/java/rearth/oritech/block/entity/arcane/EnchanterBlockEntity.java
branch: 1.21
back to repo
package rearth.oritech.block.entity.arcane;

import dev.architectury.registry.menu.ExtendedMenuProvider;
import io.wispforest.owo.util.VectorRandomUtils;
import net.minecraft.block.BlockState;
import net.minecraft.block.entity.BlockEntity;
import net.minecraft.block.entity.BlockEntityTicker;
import net.minecraft.enchantment.Enchantment;
import net.minecraft.entity.player.PlayerEntity;
import net.minecraft.entity.player.PlayerInventory;
import net.minecraft.inventory.Inventories;
import net.minecraft.inventory.Inventory;
import net.minecraft.item.ItemStack;
import net.minecraft.nbt.NbtCompound;
import net.minecraft.network.PacketByteBuf;
import net.minecraft.registry.RegistryKeys;
import net.minecraft.registry.RegistryWrapper;
import net.minecraft.registry.entry.RegistryEntry;
import net.minecraft.screen.ScreenHandler;
import net.minecraft.screen.ScreenHandlerType;
import net.minecraft.text.Text;
import net.minecraft.util.Identifier;
import net.minecraft.util.math.BlockPos;
import net.minecraft.util.math.Direction;
import net.minecraft.world.World;
import net.minecraft.world.chunk.ChunkStatus;
import org.jetbrains.annotations.Nullable;
import rearth.oritech.Oritech;
import rearth.oritech.api.energy.EnergyApi;
import rearth.oritech.api.energy.containers.DynamicEnergyStorage;
import rearth.oritech.api.item.ItemApi;
import rearth.oritech.api.item.containers.InOutInventoryStorage;
import rearth.oritech.client.init.ModScreens;
import rearth.oritech.client.init.ParticleContent;
import rearth.oritech.client.ui.EnchanterScreenHandler;
import rearth.oritech.init.BlockEntitiesContent;
import rearth.oritech.network.NetworkContent;
import rearth.oritech.util.AutoPlayingSoundKeyframeHandler;
import rearth.oritech.util.InventoryInputMode;
import rearth.oritech.util.InventorySlotAssignment;
import rearth.oritech.util.ScreenProvider;
import software.bernie.geckolib.animatable.GeoBlockEntity;
import software.bernie.geckolib.animatable.instance.AnimatableInstanceCache;
import software.bernie.geckolib.animation.AnimatableManager;
import software.bernie.geckolib.animation.AnimationController;
import software.bernie.geckolib.animation.PlayState;
import software.bernie.geckolib.animation.RawAnimation;
import software.bernie.geckolib.util.GeckoLibUtil;

import java.util.ArrayList;
import java.util.Collections;
import java.util.List;

public class EnchanterBlockEntity extends BlockEntity
  implements ItemApi.BlockProvider, EnergyApi.BlockProvider, GeoBlockEntity, ScreenProvider, BlockEntityTicker<EnchanterBlockEntity>, ExtendedMenuProvider {
    
    public static final RawAnimation IDLE = RawAnimation.begin().thenLoop("idle");
    public static final RawAnimation UNPOWERED = RawAnimation.begin().thenPlayAndHold("unpowered");
    public static final RawAnimation WORKING = RawAnimation.begin().thenPlay("working");
    
    public record EnchanterStatistics(int requiredCatalysts, int availableCatalysts){
        public static EnchanterStatistics EMPTY = new EnchanterStatistics(-1, -1);
    }
    
    protected final DynamicEnergyStorage energyStorage = new DynamicEnergyStorage(50000, 1000, 0, this::markDirty);
    
    public final InOutInventoryStorage inventory = new InOutInventoryStorage(2, this::markDirty, new InventorySlotAssignment(0, 1, 1, 1));
    
    protected final AnimatableInstanceCache animatableInstanceCache = GeckoLibUtil.createInstanceCache(this);
    public RegistryEntry<Enchantment> selectedEnchantment;
    public int progress;
    public int maxProgress = 10;
    private final List<EnchantmentCatalystBlockEntity> cachedCatalysts = new ArrayList<>();
    public EnchanterStatistics statistics = EnchanterStatistics.EMPTY; // used for client display
    private boolean networkDirty = false;
    private Identifier nbtLoadedSelection;
    private String activeAnimation = "idle";
    
    public EnchanterBlockEntity(BlockPos pos, BlockState state) {
        super(BlockEntitiesContent.ENCHANTER_BLOCK_ENTITY, pos, state);
    }
    
    @Override
    public void tick(World world, BlockPos pos, BlockState state, EnchanterBlockEntity blockEntity) {
        
        if (world.isClient) return;
        
        if (networkDirty)
            updateNetwork();
        
        activeAnimation = "idle";
        // load data from nbt, as the registry entry is not available during the readNbt method
        if (nbtLoadedSelection != null && selectedEnchantment == null) {
            var registry = world.getRegistryManager().get(RegistryKeys.ENCHANTMENT);
            var selected = registry.getEntry(registry.get(nbtLoadedSelection));
            if (selected != null)
                selectedEnchantment = selected;
            nbtLoadedSelection = null;
        }
        
        // return early if there is no work to do
        statistics = EnchanterStatistics.EMPTY;
        var content = inventory.heldStacks.get(0);
        if (content.isEmpty()
              || !inventory.getStack(1).isEmpty()
              || !content.getItem().isEnchantable(content)
              || selectedEnchantment == null
              || !selectedEnchantment.value().isAcceptableItem(content)) {
            progress = 0;
            return;
        }
        
        var existingLevel = content.getEnchantments().getLevel(selectedEnchantment);
        var maxLevel = selectedEnchantment.value().getMaxLevel();
        
        if (existingLevel >= maxLevel) return;
        
        maxProgress = getEnchantmentCost(selectedEnchantment.value(), existingLevel + 1);
        
        if (canProgress(existingLevel + 1)) {
            this.markDirty();
            energyStorage.amount -= (long) getDisplayedEnergyUsage();
            progress++;
            activeAnimation = "working";
            
            var center = pos.toCenterPos();
            var offset = VectorRandomUtils.getRandomOffset(world, center, 4f);
            ParticleContent.WEED_KILLER.spawn(world, center, new ParticleContent.LineData(center, offset));
            
            if (progress >= maxProgress) {
                progress = 0;
                finishEnchanting();
                ParticleContent.ASSEMBLER_WORKING.spawn(world, pos.toCenterPos(), maxProgress + 10);
                activeAnimation = "idle";
            }
        }
        
        if (networkDirty) {
            updateNetwork();
            updateAnimation();
        }
        
    }
    
    @Override
    protected void writeNbt(NbtCompound nbt, RegistryWrapper.WrapperLookup registryLookup) {
        super.writeNbt(nbt, registryLookup);
        Inventories.writeNbt(nbt, inventory.heldStacks, false, registryLookup);
        nbt.putLong("energy", energyStorage.amount);
        if (selectedEnchantment != null) {
            nbt.putString("selected", selectedEnchantment.getIdAsString());
        } else {
            nbt.remove("selected");
        }
    }
    
    @Override
    protected void readNbt(NbtCompound nbt, RegistryWrapper.WrapperLookup registryLookup) {
        super.readNbt(nbt, registryLookup);
        Inventories.readNbt(nbt, inventory.heldStacks, registryLookup);
        energyStorage.amount = nbt.getLong("energy");
        
        if (nbt.contains("selected")) {
            nbtLoadedSelection = Identifier.of(nbt.getString("selected"));
        }
    }
    
    private void finishEnchanting() {
        var content = inventory.heldStacks.get(0);
        var existingLevel = content.getEnchantments().getLevel(selectedEnchantment);
        content.addEnchantment(selectedEnchantment, existingLevel + 1);
        
        inventory.heldStacks.set(0, ItemStack.EMPTY);
        inventory.heldStacks.set(1, content);
        statistics = new EnchanterStatistics(0, cachedCatalysts.size());
    }
    
    private int getRequiredCatalystCount(int targetLevel) {
        return selectedEnchantment.value().getAnvilCost() + targetLevel;
    }
    
    private boolean canProgress(int targetLevel) {
        networkDirty = true;
        if (energyStorage.amount <= getDisplayedEnergyUsage()) {
            activeAnimation = "unpowered";
            return false;
        }
        
        if (world.getTime() % 15 == 0) updateNearbyCatalysts();
        var requiredCatalysts = getRequiredCatalystCount(targetLevel);
        
        statistics = new EnchanterStatistics(requiredCatalysts, cachedCatalysts.size());
        
        for (var catalyst : cachedCatalysts) {
            ParticleContent.CATALYST_CONNECTION.spawn(world, pos.toCenterPos(), new ParticleContent.LineData(catalyst.getPos().toCenterPos(), pos.up().toCenterPos()));
        }
        
        if (cachedCatalysts.size() < requiredCatalysts) return false;
        
        // get a random entry where souls > 0
        Collections.shuffle(cachedCatalysts);
        var usedOne = cachedCatalysts.stream().filter(elem -> elem.collectedSouls > 0).findFirst();
        if (usedOne.isEmpty()) return false;
        
        usedOne.get().collectedSouls--;
        
        return true;
    }
    
    private int getEnchantmentCost(Enchantment enchantment, int targetLevel) {
        return enchantment.getAnvilCost() * targetLevel * Oritech.CONFIG.enchanterCostMultiplier() + 1;
    }
    
    public void handleEnchantmentSelection(NetworkContent.EnchanterSelectionPacket packet) {
        
        if (packet.enchantment().isEmpty()) {
            selectedEnchantment = null;
            return;
        }
        
        var registry = world.getRegistryManager().get(RegistryKeys.ENCHANTMENT);
        var selected = registry.getEntry(registry.get(Identifier.of(packet.enchantment())));
        if (selected != null)
            selectedEnchantment = selected;
    }
    
    public void handleSyncPacket(NetworkContent.EnchanterSyncPacket message) {
        
        this.progress = message.progress();
        this.maxProgress = message.maxProgress();
        this.energyStorage.amount = message.energy();
        this.statistics = new EnchanterStatistics(message.requiredCatalysts(), message.availableCatalysts());
        
    }
    
    @Override
    public void registerControllers(AnimatableManager.ControllerRegistrar controllers) {
        controllers.add(new AnimationController<>(this, "machine", 4, state -> PlayState.CONTINUE)
                          .triggerableAnim("working", WORKING)
                          .triggerableAnim("idle", IDLE)
                          .triggerableAnim("unpowered", UNPOWERED)
                          .setSoundKeyframeHandler(new AutoPlayingSoundKeyframeHandler<>()));
    }
    
    @Override
    public AnimatableInstanceCache getAnimatableInstanceCache() {
        return animatableInstanceCache;
    }
    
    private void updateAnimation() {
        triggerAnim("machine", activeAnimation);
    }
    
    private void updateNetwork() {
        networkDirty = false;
        NetworkContent.MACHINE_CHANNEL.serverHandle(this).send(new NetworkContent.EnchanterSyncPacket(pos, energyStorage.amount, progress, maxProgress, statistics.requiredCatalysts, statistics.availableCatalysts));
    }
    
    @Override
    public void markDirty() {
        super.markDirty();
        networkDirty = true;
    }
    
    private void updateNearbyCatalysts() {
        var chunkRadius = 1;
        
        var startX = (pos.getX() >> 4) - chunkRadius;
        var startZ = (pos.getZ() >> 4) - chunkRadius;
        var endX = (pos.getX() >> 4) + chunkRadius;
        var endZ = (pos.getZ() >> 4) + chunkRadius;
        
        cachedCatalysts.clear();
        
        for (int chunkX = startX; chunkX <= endX; chunkX++) {
            for (int chunkZ = startZ; chunkZ <= endZ; chunkZ++) {
                var chunk = world.getChunk(chunkX, chunkZ, ChunkStatus.FULL, false);
                if (chunk == null) continue;
                
                var entities = chunk.blockEntities;
                // select all non-empty catalysts within range (16)
                var catalysts = entities.values()
                                  .stream()
                                  .filter(elem -> elem instanceof EnchantmentCatalystBlockEntity catalyst && catalyst.collectedSouls > 0 && elem.getPos().getManhattanDistance(pos) < 16)
                                  .map(elem -> (EnchantmentCatalystBlockEntity) elem)
                                  .toList();
                cachedCatalysts.addAll(catalysts);
            }
        }
    }
    
    @Override
    public void saveExtraData(PacketByteBuf buf) {
        networkDirty = true;
        buf.writeBlockPos(pos);
    }
    
    @Override
    public Text getDisplayName() {
        return Text.literal("");
    }
    
    @Nullable
    @Override
    public ScreenHandler createMenu(int syncId, PlayerInventory playerInventory, PlayerEntity player) {
        sendSelectionToClient();
        networkDirty = true;
        return new EnchanterScreenHandler(syncId, playerInventory, this);
    }
    
    private void sendSelectionToClient() {
        if (selectedEnchantment == null) return;
        NetworkContent.MACHINE_CHANNEL.serverHandle(this).send(new NetworkContent.EnchanterSelectionPacket(pos, selectedEnchantment.getIdAsString()));
    }
    
    @Override
    public EnergyApi.EnergyStorage getEnergyStorage(Direction direction) {
        return energyStorage;
    }
    
    @Override
    public List<GuiSlot> getGuiSlots() {
        return List.of(
          new GuiSlot(0, 52, 58),
          new GuiSlot(1, 108, 58, true));
    }
    
    @Override
    public ArrowConfiguration getIndicatorConfiguration() {
        return new ArrowConfiguration(
          Oritech.id("textures/gui/modular/arrow_empty.png"),
          Oritech.id("textures/gui/modular/arrow_full.png"),
          73, 58, 29, 16, true);
    }
    
    @Override
    public BarConfiguration getEnergyConfiguration() {
        return new BarConfiguration(7, 7, 18, 71);
    }
    
    @Override
    public float getDisplayedEnergyUsage() {
        return 512; // todo config parameter
    }
    
    @Override
    public float getProgress() {
        return (float) progress / maxProgress;
    }
    
    @Override
    public InventoryInputMode getInventoryInputMode() {
        return InventoryInputMode.FILL_LEFT_TO_RIGHT;
    }
    
    @Override
    public Inventory getDisplayedInventory() {
        return inventory;
    }
    
    @Override
    public ScreenHandlerType<?> getScreenHandlerType() {
        return ModScreens.ENCHANTER_SCREEN;
    }
    
    @Override
    public boolean inputOptionsEnabled() {
        return false;
    }
    
    @Override
    public ItemApi.InventoryStorage getInventoryStorage(Direction direction) {
        return inventory;
    }
}