filename:
common/src/main/java/rearth/oritech/block/entity/arcane/EnchantmentCatalystBlockEntity.java
branch:
1.21
back to repo
package rearth.oritech.block.entity.arcane;
import dev.architectury.registry.menu.ExtendedMenuProvider;
import net.minecraft.block.BlockState;
import net.minecraft.block.entity.BlockEntityTicker;
import net.minecraft.component.DataComponentTypes;
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.item.Items;
import net.minecraft.nbt.NbtCompound;
import net.minecraft.network.PacketByteBuf;
import net.minecraft.registry.RegistryWrapper;
import net.minecraft.screen.ScreenHandler;
import net.minecraft.screen.ScreenHandlerType;
import net.minecraft.text.Text;
import net.minecraft.util.math.BlockPos;
import net.minecraft.util.math.Direction;
import net.minecraft.util.math.Vec3d;
import net.minecraft.world.World;
import org.jetbrains.annotations.Nullable;
import rearth.oritech.Oritech;
import rearth.oritech.api.energy.EnergyApi;
import rearth.oritech.api.energy.containers.SimpleEnergyStorage;
import rearth.oritech.api.item.ItemApi;
import rearth.oritech.api.item.containers.SimpleInventoryStorage;
import rearth.oritech.client.init.ModScreens;
import rearth.oritech.client.init.ParticleContent;
import rearth.oritech.client.ui.CatalystScreenHandler;
import rearth.oritech.init.BlockEntitiesContent;
import rearth.oritech.network.NetworkContent;
import rearth.oritech.util.AutoPlayingSoundKeyframeHandler;
import rearth.oritech.util.ComparatorOutputProvider;
import rearth.oritech.util.InventoryInputMode;
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.List;
public class EnchantmentCatalystBlockEntity extends BaseSoulCollectionEntity
implements ItemApi.BlockProvider, EnergyApi.BlockProvider, ScreenProvider, ComparatorOutputProvider, GeoBlockEntity, BlockEntityTicker<EnchantmentCatalystBlockEntity>, ExtendedMenuProvider {
public static final RawAnimation IDLE = RawAnimation.begin().thenLoop("idle");
public static final RawAnimation STABILIZED = RawAnimation.begin().thenLoop("stabilized");
public static final RawAnimation UNSTABLE = RawAnimation.begin().thenLoop("unstable");
public final int baseSoulCapacity = Oritech.CONFIG.catalystBaseSouls();
public final int maxProgress = 20;
protected final AnimatableInstanceCache animatableInstanceCache = GeckoLibUtil.createInstanceCache(this);
// working data
public int collectedSouls;
public int maxSouls = Oritech.CONFIG.catalystBaseSouls();
private int unstableTicks;
private int progress;
private boolean isHyperEnchanting;
private boolean networkDirty;
private String lastAnimation = "idle";
private int lastComparatorOutput;
public final SimpleInventoryStorage inventory = new SimpleInventoryStorage(2, this::markDirty) {
@Override
public int insertToSlot(ItemStack addedStack, int slot, boolean simulate) {
if (slot == 0 && !addedStack.isEmpty() && !addedStack.getItem().equals(Items.ENCHANTED_BOOK)) return 0; // only allow enchanter books in slot 0
return super.insertToSlot(addedStack, slot, simulate);
}
};
public final SimpleEnergyStorage energyStorage = new SimpleEnergyStorage(10_000, 0, 50_000);
public EnchantmentCatalystBlockEntity(BlockPos pos, BlockState state) {
super(BlockEntitiesContent.ENCHANTMENT_CATALYST_BLOCK_ENTITY, pos, state);
}
@Override
public void tick(World world, BlockPos pos, BlockState state, EnchantmentCatalystBlockEntity blockEntity) {
if (world.isClient) return;
// check if powered, and adjust soul capacity
if (energyStorage.getAmount() > 0) {
var gainedSoulCapacity = energyStorage.getAmount() / Oritech.CONFIG.catalystRFPerSoul();
energyStorage.setAmount(0);
var newMax = baseSoulCapacity + gainedSoulCapacity;
adjustMaxSouls(newMax);
this.markDirty();
} else if (maxSouls > baseSoulCapacity) {
adjustMaxSouls(baseSoulCapacity);
}
// explode if unstable
if (collectedSouls > maxSouls) {
unstableTicks++;
ParticleContent.MELTDOWN_IMMINENT.spawn(world, pos.toCenterPos(), unstableTicks / 4);
if (unstableTicks > 60)
doExplosion();
return;
}
unstableTicks = 0;
// check if output is empty
// check if a book is in slot 0
// check if an item is in slot 1
if (canProceed()) {
networkDirty = true;
progress++;
ParticleContent.SOUL_USED.spawn(world, pos.toCenterPos().add(0, 0.3, 0), isHyperEnchanting ? 15 : 3);
if (progress >= maxProgress) {
enchantInput();
ParticleContent.ASSEMBLER_WORKING.spawn(world, pos.toCenterPos(), maxProgress + 10);
progress = 0;
isHyperEnchanting = false;
}
} else {
progress = 0;
}
if (networkDirty) {
networkDirty = false;
updateNetwork();
DeathListener.resetEvents();
updateAnimation();
var level = calculateComparatorLevel();
if (level != lastComparatorOutput) {
lastComparatorOutput = level;
world.updateComparators(pos, state.getBlock());
}
}
// periodically re-trigger animation updates
if (world.getTime() % 60 == 0)
lastAnimation = "idle";
}
@Override
protected void writeNbt(NbtCompound nbt, RegistryWrapper.WrapperLookup registryLookup) {
super.writeNbt(nbt, registryLookup);
Inventories.writeNbt(nbt, inventory.heldStacks, false, registryLookup);
nbt.putInt("souls", collectedSouls);
nbt.putInt("maxSouls", maxSouls);
}
@Override
protected void readNbt(NbtCompound nbt, RegistryWrapper.WrapperLookup registryLookup) {
super.readNbt(nbt, registryLookup);
Inventories.readNbt(nbt, inventory.heldStacks, registryLookup);
collectedSouls = nbt.getInt("souls");
maxSouls = nbt.getInt("maxSouls");
}
private void doExplosion() {
var center = pos.toCenterPos();
var strength = Math.sqrt(collectedSouls - baseSoulCapacity);
world.createExplosion(null, center.x, center.y, center.z, (int) strength, true, World.ExplosionSourceType.BLOCK);
world.removeBlock(pos, false);
}
private void adjustMaxSouls(long target) {
if (maxSouls > target) {
maxSouls--;
} else if (maxSouls < target) {
maxSouls++;
}
this.networkDirty = true;
}
private void enchantInput() {
var bookCandidate = inventory.getStack(0);
if (!bookCandidate.getItem().equals(Items.ENCHANTED_BOOK) || !bookCandidate.contains(DataComponentTypes.STORED_ENCHANTMENTS))
return;
var enchantment = bookCandidate.get(DataComponentTypes.STORED_ENCHANTMENTS).getEnchantments().stream().findFirst().get();
var inputStack = inventory.getStack(1);
var toolLevel = inputStack.getEnchantments().getLevel(enchantment);
inputStack.addEnchantment(enchantment, toolLevel + 1);
collectedSouls -= getEnchantmentCost(enchantment.value(), toolLevel + 1, isHyperEnchanting);
if (isHyperEnchanting)
inventory.setStack(0, ItemStack.EMPTY);
}
private boolean hasEnoughSouls(Enchantment enchantment, int targetLevel) {
var resultingCost = getEnchantmentCost(enchantment, targetLevel, isHyperEnchanting);
return collectedSouls >= resultingCost;
}
private int getEnchantmentCost(Enchantment enchantment, int targetLevel, boolean hyper) {
var baseCost = enchantment.getAnvilCost();
var resultingCost = baseCost * targetLevel * Oritech.CONFIG.catalystCostMultiplier();
if (hyper) resultingCost = resultingCost * Oritech.CONFIG.catalystHyperMultiplier() + Oritech.CONFIG.catalystBaseSouls();
return resultingCost;
}
// for UI
public int getDisplayedCost() {
if (inventory.getStack(0).isEmpty() || inventory.getStack(1).isEmpty()) return 0;
var bookCandidate = inventory.getStack(0);
if (bookCandidate.getItem().equals(Items.ENCHANTED_BOOK) && bookCandidate.contains(DataComponentTypes.STORED_ENCHANTMENTS)) {
var enchantment = bookCandidate.get(DataComponentTypes.STORED_ENCHANTMENTS).getEnchantments().stream().findFirst().get();
var maxLevel = enchantment.value().getMaxLevel();
var bookLevel = bookCandidate.get(DataComponentTypes.STORED_ENCHANTMENTS).getLevel(enchantment);
if (bookLevel != maxLevel) return 0;
var inputStack = inventory.getStack(1);
var toolLevel = inputStack.getEnchantments().getLevel(enchantment);
var isHyper = toolLevel >= maxLevel;
return getEnchantmentCost(enchantment.value(), toolLevel + 1, isHyper);
}
return 0;
}
private boolean canProceed() {
if (inventory.getStack(0).isEmpty() || inventory.getStack(1).isEmpty()) return false;
var bookCandidate = inventory.getStack(0);
if (bookCandidate.getItem().equals(Items.ENCHANTED_BOOK) && bookCandidate.contains(DataComponentTypes.STORED_ENCHANTMENTS)) {
var enchantment = bookCandidate.get(DataComponentTypes.STORED_ENCHANTMENTS).getEnchantments().stream().findFirst().get();
var maxLevel = enchantment.value().getMaxLevel();
var level = bookCandidate.get(DataComponentTypes.STORED_ENCHANTMENTS).getLevel(enchantment);
// yes this does not check if the item can be enchanted with this enchantment. This is intentional, allowing you to skip the normal limitations
var inputStack = inventory.getStack(1);
var toolLevel = inputStack.getEnchantments().getLevel(enchantment);
this.isHyperEnchanting = toolLevel >= maxLevel;
return level == maxLevel && hasEnoughSouls(enchantment.value(), toolLevel + 1);
}
return false;
}
@Override
public void onSoulIncoming(Vec3d source) {
var distance = (float) source.distanceTo(pos.toCenterPos());
collectedSouls++;
networkDirty = true;
var soulPath = pos.toCenterPos().subtract(source);
var animData = new ParticleContent.SoulParticleData(soulPath, (int) getSoulTravelDuration(distance));
ParticleContent.WANDERING_SOUL.spawn(world, source.add(0, 0.7f, 0), animData);
}
@Override
public boolean canAcceptSoul() {
return collectedSouls < maxSouls;
}
@Override
public int getComparatorOutput() {
return calculateComparatorLevel();
}
private int calculateComparatorLevel() {
return (int) ((float) collectedSouls / maxSouls * 16);
}
private void updateNetwork() {
NetworkContent.MACHINE_CHANNEL.serverHandle(this).send(new NetworkContent.CatalystSyncPacket(pos, collectedSouls, progress, isHyperEnchanting, maxSouls));
}
public void handleNetworkPacket(NetworkContent.CatalystSyncPacket packet) {
this.isHyperEnchanting = packet.isHyperEnchanting();
this.progress = packet.progress();
this.collectedSouls = packet.storedSouls();
this.maxSouls = packet.maxSouls();
}
@Override
public void saveExtraData(PacketByteBuf buf) {
buf.writeBlockPos(pos);
}
@Override
public Text getDisplayName() {
return Text.literal("");
}
@Override
public boolean showProgress() {
return false;
}
@Nullable
@Override
public ScreenHandler createMenu(int syncId, PlayerInventory playerInventory, PlayerEntity player) {
updateNetwork();
return new CatalystScreenHandler(syncId, playerInventory, this);
}
@Override
public ItemApi.InventoryStorage getInventoryStorage(Direction direction) {
return inventory;
}
@Override
public EnergyApi.EnergyStorage getEnergyStorage(Direction direction) {
return energyStorage;
}
@Override
public List<GuiSlot> getGuiSlots() {
return List.of(
new GuiSlot(0, 56, 35),
new GuiSlot(1, 75, 35));
}
@Override
public BarConfiguration getEnergyConfiguration() {
return new BarConfiguration(7, 7, 18, 71);
}
@Override
public void registerControllers(AnimatableManager.ControllerRegistrar controllers) {
controllers.add(new AnimationController<>(this, "machine", 4, state -> {
if (state.getController().getAnimationState().equals(AnimationController.State.STOPPED))
return state.setAndContinue(IDLE);
return PlayState.CONTINUE;
})
.triggerableAnim("stabilized", STABILIZED)
.triggerableAnim("idle", IDLE)
.triggerableAnim("unstable", UNSTABLE)
.setSoundKeyframeHandler(new AutoPlayingSoundKeyframeHandler<>()));
}
private void updateAnimation() {
var targetAnim = "idle";
if (maxSouls > baseSoulCapacity)
targetAnim = "stabilized";
if (unstableTicks > 0)
targetAnim = "unstable";
if (!targetAnim.equals(lastAnimation)) {
triggerAnim("machine", targetAnim);
lastAnimation = targetAnim;
}
}
@Override
public AnimatableInstanceCache getAnimatableInstanceCache() {
return animatableInstanceCache;
}
@Override
public float getDisplayedEnergyUsage() {
return 0;
}
@Override
public float getProgress() {
return progress / (float) maxProgress;
}
@Override
public InventoryInputMode getInventoryInputMode() {
return InventoryInputMode.FILL_LEFT_TO_RIGHT;
}
@Override
public Inventory getDisplayedInventory() {
return inventory;
}
@Override
public ScreenHandlerType<?> getScreenHandlerType() {
return ModScreens.CATALYST_SCREEN;
}
@Override
public boolean inputOptionsEnabled() {
return false;
}
// this is used as soul display instead
@Override
public boolean showEnergy() {
return true;
}
}