HexCasting/Common/src/main/java/at/petrak/hexcasting/api/casting/circles/BlockEntityAbstractImpetus.java

436 lines
14 KiB
Java

package at.petrak.hexcasting.api.casting.circles;
import at.petrak.hexcasting.api.block.HexBlockEntity;
import at.petrak.hexcasting.api.block.circle.BlockCircleComponent;
import at.petrak.hexcasting.api.misc.MediaConstants;
import at.petrak.hexcasting.api.utils.MediaHelper;
import at.petrak.hexcasting.common.items.magic.ItemCreativeUnlocker;
import at.petrak.hexcasting.common.lib.HexItems;
import com.mojang.datafixers.util.Pair;
import net.minecraft.ChatFormatting;
import net.minecraft.core.BlockPos;
import net.minecraft.core.Direction;
import net.minecraft.nbt.CompoundTag;
import net.minecraft.nbt.Tag;
import net.minecraft.network.chat.Component;
import net.minecraft.server.level.ServerLevel;
import net.minecraft.server.level.ServerPlayer;
import net.minecraft.tags.BlockTags;
import net.minecraft.util.Mth;
import net.minecraft.world.WorldlyContainer;
import net.minecraft.world.entity.player.Player;
import net.minecraft.world.item.ItemStack;
import net.minecraft.world.item.Items;
import net.minecraft.world.level.Level;
import net.minecraft.world.level.block.Blocks;
import net.minecraft.world.level.block.entity.BlockEntityType;
import net.minecraft.world.level.block.state.BlockState;
import net.minecraft.world.level.block.state.properties.BlockStateProperties;
import net.minecraft.world.phys.AABB;
import org.jetbrains.annotations.Contract;
import org.jetbrains.annotations.Nullable;
import java.text.DecimalFormat;
import java.util.List;
/**
* Default impl for an impetus, not tecnically necessary but I'm exposing it for ease of use
* <p>
* This does assume a great deal so you might have to re-implement a lot of this yourself if you
* wanna do something wild and new
*/
public abstract class BlockEntityAbstractImpetus extends HexBlockEntity implements WorldlyContainer {
private static final DecimalFormat DUST_AMOUNT = new DecimalFormat("###,###.##");
private static final long MAX_CAPACITY = 9_000_000_000_000_000_000L;
public static final String
TAG_EXECUTION_STATE = "executor",
TAG_MEDIA = "media",
TAG_ERROR_MSG = "errorMsg",
TAG_ERROR_DISPLAY = "errorDisplay";
// We might try to load the executor in loadModData when the level doesn't exist yet,
// so save the tag and load it lazy
@Nullable CompoundTag lazyExecutionState;
@Nullable
protected CircleExecutionState executionState;
protected long media = 0;
// these are null together
@Nullable
protected Component displayMsg = null;
@Nullable
protected ItemStack displayItem = null;
public BlockEntityAbstractImpetus(BlockEntityType<?> pType, BlockPos pWorldPosition, BlockState pBlockState) {
super(pType, pWorldPosition, pBlockState);
}
public Direction getStartDirection() {
return this.getBlockState().getValue(BlockStateProperties.FACING);
}
@Nullable
public Component getDisplayMsg() {
return displayMsg;
}
public void clearDisplay() {
this.displayMsg = null;
this.displayItem = null;
this.sync();
}
public void postDisplay(Component error, ItemStack display) {
this.displayMsg = error;
this.displayItem = display;
this.sync();
}
public void postMishap(Component mishapDisplay) {
this.postDisplay(mishapDisplay, new ItemStack(Items.MUSIC_DISC_11));
}
public void postPrint(Component printDisplay) {
this.postDisplay(printDisplay, new ItemStack(Items.BOOK));
}
// Pull this out because we may need to call it both on startup and halfway thru
public void postNoExits(BlockPos pos) {
this.postDisplay(
Component.translatable("hexcasting.tooltip.circle.no_exit",
Component.literal(pos.toShortString()).withStyle(ChatFormatting.RED)),
new ItemStack(Items.OAK_SIGN));
}
//region execution
public void tickExecution() {
if (this.level == null)
return;
this.setChanged();
var state = this.getExecutionState();
if (state == null) {
return;
}
var shouldContinue = state.tick(this);
if (!shouldContinue) {
this.endExecution();
this.executionState = null;
} else
this.level.scheduleTick(this.getBlockPos(), this.getBlockState().getBlock(), state.getTickSpeed());
}
public void endExecution() {
if (this.executionState == null)
return;
this.executionState.endExecution(this);
}
/**
* ONLY CALL THIS WHEN YOU KNOW THE WORLD EXISTS AND ON THE SERVER, lazy-loads it
*/
public @Nullable CircleExecutionState getExecutionState() {
if (this.level == null) {
throw new IllegalStateException("didn't you read the doc comment, don't call this if the level is null");
}
if (this.executionState != null)
return this.executionState;
if (this.lazyExecutionState != null)
this.executionState = CircleExecutionState.load(this.lazyExecutionState, (ServerLevel) this.level);
return this.executionState;
}
public void startExecution(@Nullable ServerPlayer player) {
if (this.level == null)
return; // TODO: error here?
if (this.level.isClientSide)
return; // TODO: error here?
if (this.executionState != null) {
return;
}
var result = CircleExecutionState.createNew(this, player);
if (result.isErr()) {
var errPos = result.unwrapErr();
if (errPos == null) {
ICircleComponent.sfx(this.getBlockPos(), this.getBlockState(), this.level, null, false);
this.postNoExits(this.getBlockPos());
} else {
ICircleComponent.sfx(errPos, this.level.getBlockState(errPos), this.level, null, false);
this.postDisplay(Component.translatable("hexcasting.tooltip.circle.no_closure",
Component.literal(errPos.toShortString()).withStyle(ChatFormatting.RED)),
new ItemStack(Items.LEAD));
}
return;
}
this.executionState = result.unwrap();
this.clearDisplay();
var serverLevel = (ServerLevel) this.level;
serverLevel.scheduleTick(this.getBlockPos(), this.getBlockState().getBlock(),
this.executionState.getTickSpeed());
serverLevel.setBlockAndUpdate(this.getBlockPos(),
this.getBlockState().setValue(BlockCircleComponent.ENERGIZED, true));
}
@Contract(pure = true)
protected static AABB getBounds(List<BlockPos> poses) {
int minX = Integer.MAX_VALUE;
int minY = Integer.MAX_VALUE;
int minZ = Integer.MAX_VALUE;
int maxX = Integer.MIN_VALUE;
int maxY = Integer.MIN_VALUE;
int maxZ = Integer.MIN_VALUE;
for (var pos : poses) {
if (pos.getX() < minX) {
minX = pos.getX();
}
if (pos.getY() < minY) {
minY = pos.getY();
}
if (pos.getZ() < minZ) {
minZ = pos.getZ();
}
if (pos.getX() > maxX) {
maxX = pos.getX();
}
if (pos.getY() > maxY) {
maxY = pos.getY();
}
if (pos.getZ() > maxZ) {
maxZ = pos.getZ();
}
}
return new AABB(minX, minY, minZ, maxX + 1, maxY + 1, maxZ + 1);
}
//endregion
//region media handling
public long getMedia() {
return this.media;
}
public void setMedia(long media) {
this.media = media;
}
public long extractMediaFromInsertedItem(ItemStack stack, boolean simulate) {
if (this.media < 0) {
return 0;
}
return MediaHelper.extractMedia(stack, remainingMediaCapacity(), true, simulate);
}
public void insertMedia(ItemStack stack) {
if (getMedia() >= 0 && !stack.isEmpty() && stack.getItem() == HexItems.CREATIVE_UNLOCKER) {
setInfiniteMedia();
stack.shrink(1);
} else {
var mediamount = extractMediaFromInsertedItem(stack, false);
if (mediamount > 0) {
this.media = Math.min(mediamount + media, MAX_CAPACITY);
this.sync();
}
}
}
public void setInfiniteMedia() {
this.media = -1;
this.sync();
}
public long remainingMediaCapacity() {
if (this.media < 0) {
return 0;
}
return Math.max(0, MAX_CAPACITY - this.media);
}
//endregion
@Override
protected void saveModData(CompoundTag tag) {
if (this.executionState != null) {
tag.put(TAG_EXECUTION_STATE, this.executionState.save());
}
tag.putLong(TAG_MEDIA, this.media);
if (this.displayMsg != null && this.displayItem != null) {
tag.putString(TAG_ERROR_MSG, Component.Serializer.toJson(this.displayMsg));
var itemTag = new CompoundTag();
this.displayItem.save(itemTag);
tag.put(TAG_ERROR_DISPLAY, itemTag);
}
}
@Override
protected void loadModData(CompoundTag tag) {
this.executionState = null;
if (tag.contains(TAG_EXECUTION_STATE, Tag.TAG_COMPOUND)) {
this.lazyExecutionState = tag.getCompound(TAG_EXECUTION_STATE);
} else {
this.lazyExecutionState = null;
}
if (tag.contains(TAG_MEDIA, Tag.TAG_LONG)) {
this.media = tag.getLong(TAG_MEDIA);
}
if (tag.contains(TAG_ERROR_MSG, Tag.TAG_STRING) && tag.contains(TAG_ERROR_DISPLAY, Tag.TAG_COMPOUND)) {
var msg = Component.Serializer.fromJson(tag.getString(TAG_ERROR_MSG));
var display = ItemStack.of(tag.getCompound(TAG_ERROR_DISPLAY));
this.displayMsg = msg;
this.displayItem = display;
} else {
this.displayMsg = null;
this.displayItem = null;
}
}
public void applyScryingLensOverlay(List<Pair<ItemStack, Component>> lines,
BlockState state, BlockPos pos, Player observer, Level world, Direction hitFace) {
if (world.getBlockEntity(pos) instanceof BlockEntityAbstractImpetus beai) {
if (beai.getMedia() < 0) {
lines.add(new Pair<>(new ItemStack(HexItems.AMETHYST_DUST), ItemCreativeUnlocker.infiniteMedia(world)));
} else {
var dustCount = (float) beai.getMedia() / (float) MediaConstants.DUST_UNIT;
var dustCmp = Component.translatable("hexcasting.tooltip.media",
DUST_AMOUNT.format(dustCount));
lines.add(new Pair<>(new ItemStack(HexItems.AMETHYST_DUST), dustCmp));
}
if (this.displayMsg != null && this.displayItem != null) {
lines.add(new Pair<>(this.displayItem, this.displayMsg));
}
}
}
//region music
protected int semitoneFromScale(int note) {
var blockBelow = this.level.getBlockState(this.getBlockPos().below());
var scale = MAJOR_SCALE;
if (blockBelow.is(Blocks.CRYING_OBSIDIAN)) {
scale = MINOR_SCALE;
} else if (blockBelow.is(BlockTags.DOORS) || blockBelow.is(BlockTags.TRAPDOORS)) {
scale = DORIAN_SCALE;
} else if (blockBelow.is(Blocks.PISTON) || blockBelow.is(Blocks.STICKY_PISTON)) {
scale = MIXOLYDIAN_SCALE;
} else if (blockBelow.is(Blocks.BLUE_WOOL)
|| blockBelow.is(Blocks.BLUE_CONCRETE) || blockBelow.is(Blocks.BLUE_CONCRETE_POWDER)
|| blockBelow.is(Blocks.BLUE_TERRACOTTA) || blockBelow.is(Blocks.BLUE_GLAZED_TERRACOTTA)
|| blockBelow.is(Blocks.BLUE_STAINED_GLASS) || blockBelow.is(Blocks.BLUE_STAINED_GLASS_PANE)) {
scale = BLUES_SCALE;
} else if (blockBelow.is(Blocks.BONE_BLOCK)) {
scale = BAD_TIME;
} else if (blockBelow.is(Blocks.COMPOSTER)) {
scale = SUSSY_BAKA;
}
note = Mth.clamp(note, 0, scale.length - 1);
return scale[note];
}
// this is a good use of my time
private static final int[] MAJOR_SCALE = {0, 2, 4, 5, 7, 9, 11, 12};
private static final int[] MINOR_SCALE = {0, 2, 3, 5, 7, 8, 11, 12};
private static final int[] DORIAN_SCALE = {0, 2, 3, 5, 7, 9, 10, 12};
private static final int[] MIXOLYDIAN_SCALE = {0, 2, 4, 5, 7, 9, 10, 12};
private static final int[] BLUES_SCALE = {0, 3, 5, 6, 7, 10, 12};
private static final int[] BAD_TIME = {0, 0, 12, 7, 6, 5, 3, 0, 3, 5};
private static final int[] SUSSY_BAKA = {5, 8, 10, 11, 10, 8, 5, 3, 7, 5};
//endregion
//region item handler contract stuff
private static final int[] SLOTS = {0};
@Override
public int[] getSlotsForFace(Direction var1) {
return SLOTS;
}
@Override
public boolean canPlaceItemThroughFace(int index, ItemStack stack, @Nullable Direction dir) {
return this.canPlaceItem(index, stack);
}
@Override
public boolean canTakeItemThroughFace(int var1, ItemStack var2, Direction var3) {
return false;
}
@Override
public int getContainerSize() {
return 1;
}
@Override
public boolean isEmpty() {
return true;
}
@Override
public ItemStack getItem(int index) {
return ItemStack.EMPTY.copy();
}
@Override
public ItemStack removeItem(int index, int count) {
return ItemStack.EMPTY.copy();
}
@Override
public ItemStack removeItemNoUpdate(int index) {
return ItemStack.EMPTY.copy();
}
@Override
public void setItem(int index, ItemStack stack) {
insertMedia(stack);
}
@Override
public boolean stillValid(Player player) {
return false;
}
@Override
public void clearContent() {
// NO-OP
}
@Override
public boolean canPlaceItem(int index, ItemStack stack) {
if (remainingMediaCapacity() == 0) {
return false;
}
if (stack.is(HexItems.CREATIVE_UNLOCKER)) {
return true;
}
var mediamount = extractMediaFromInsertedItem(stack, true);
return mediamount > 0;
}
//endregion
}