filename:
common/src/main/java/rearth/oritech/block/entity/accelerator/AcceleratorControllerBlockEntity.java
branch:
1.21
back to repo
package rearth.oritech.block.entity.accelerator;
import dev.architectury.registry.menu.ExtendedMenuProvider;
import io.wispforest.owo.util.VectorRandomUtils;
import net.minecraft.block.BlockState;
import net.minecraft.block.Blocks;
import net.minecraft.block.Portal;
import net.minecraft.block.entity.BlockEntity;
import net.minecraft.block.entity.BlockEntityTicker;
import net.minecraft.client.MinecraftClient;
import net.minecraft.entity.LivingEntity;
import net.minecraft.entity.player.PlayerEntity;
import net.minecraft.entity.player.PlayerInventory;
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.server.world.ServerWorld;
import net.minecraft.sound.SoundCategory;
import net.minecraft.sound.SoundEvents;
import net.minecraft.state.property.Properties;
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.util.math.Vec3i;
import net.minecraft.world.World;
import org.jetbrains.annotations.Nullable;
import rearth.oritech.Oritech;
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.AcceleratorScreenHandler;
import rearth.oritech.init.BlockContent;
import rearth.oritech.init.BlockEntitiesContent;
import rearth.oritech.init.SoundContent;
import rearth.oritech.init.recipes.RecipeContent;
import rearth.oritech.network.NetworkContent;
import rearth.oritech.util.*;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;
public class AcceleratorControllerBlockEntity extends BlockEntity implements BlockEntityTicker<AcceleratorControllerBlockEntity>, ItemApi.BlockProvider, ExtendedMenuProvider, ScreenProvider {
private AcceleratorParticleLogic.ActiveParticle particle;
public ItemStack activeItemParticle = ItemStack.EMPTY;
private AcceleratorParticleLogic particleLogic;
public final InOutInventoryStorage inventory = new InOutInventoryStorage(2, this::markDirty, new InventorySlotAssignment(0, 1, 1, 1));
// client data
public List<Vec3d> displayTrail;
public LastEventPacket lastEvent = new LastEventPacket(pos, ParticleEvent.IDLE, 0, pos, 1, ItemStack.EMPTY);
public AcceleratorControllerBlockEntity(BlockPos pos, BlockState state) {
super(BlockEntitiesContent.ACCELERATOR_CONTROLLER_BLOCK_ENTITY, pos, state);
}
@Override
public void tick(World world, BlockPos pos, BlockState state, AcceleratorControllerBlockEntity blockEntity) {
if (world.isClient) return;
initParticleLogic();
// try insert item as particle
if (particle == null && !inventory.getStack(0).isEmpty() && inventory.getStack(1).isEmpty()) {
injectParticle();
}
if (particle != null)
particleLogic.update(particle);
}
@Override
protected void writeNbt(NbtCompound nbt, RegistryWrapper.WrapperLookup registryLookup) {
super.writeNbt(nbt, registryLookup);
if (particle != null && activeItemParticle != null && activeItemParticle != ItemStack.EMPTY) {
var data = new NbtCompound();
data.putFloat("speed", particle.velocity);
data.putFloat("posX", (float) particle.position.x);
data.putFloat("posY", (float) particle.position.y);
data.putFloat("posZ", (float) particle.position.z);
data.putLong("lastGate", particle.lastGate.asLong());
data.putLong("nextGate", particle.nextGate.asLong());
data.put("item", activeItemParticle.encode(registryLookup));
nbt.put("particle", data);
} else {
nbt.remove("particle");
}
}
@Override
protected void readNbt(NbtCompound nbt, RegistryWrapper.WrapperLookup registryLookup) {
super.readNbt(nbt, registryLookup);
if (nbt.contains("particle")) {
var data = nbt.getCompound("particle");
var speed = data.getFloat("speed");
var posX = data.getFloat("posX");
var posY = data.getFloat("posY");
var posZ = data.getFloat("posZ");
var lastGate = BlockPos.fromLong(data.getLong("lastGate"));
var nextGate = BlockPos.fromLong(data.getLong("nextGate"));
var item = ItemStack.fromNbt(registryLookup, data.get("item"));
item.ifPresent(stack -> activeItemParticle = stack);
particle = new AcceleratorParticleLogic.ActiveParticle(new Vec3d(posX, posY, posZ), speed, lastGate, nextGate);
}
}
private void initParticleLogic() {
if (particleLogic == null) particleLogic = new AcceleratorParticleLogic(pos, (ServerWorld) world, this);
}
public void injectParticle() {
var facing = getCachedState().get(Properties.HORIZONTAL_FACING);
var posBehind = Geometry.offsetToWorldPosition(facing, new Vec3i(1, 0, 0), pos);
var directionRight = Geometry.getRight(facing);
var candidateBlock = world.getBlockState(new BlockPos(posBehind));
if (candidateBlock.getBlock().equals(BlockContent.ACCELERATOR_RING)) {
var startPosition = (BlockPos) posBehind;
var nextGate = particleLogic.findNextGate(startPosition, directionRight, 1);
particle = new AcceleratorParticleLogic.ActiveParticle(startPosition.toCenterPos(), 1, nextGate, startPosition);
activeItemParticle = inventory.getStack(0).split(1);
NetworkContent.MACHINE_CHANNEL.serverHandle(this).send(new NetworkContent.AcceleratorParticleInsertEventPacket(pos));
}
}
public void removeParticleDueToCollision() {
this.particle = null;
this.activeItemParticle = ItemStack.EMPTY;
}
public void onParticleExited(Vec3d from, Vec3d to, BlockPos lastGate, Vec3d exitDirection, ParticleEvent reason) {
var eventPosition = BlockPos.ofFloored(particle.position);
NetworkContent.MACHINE_CHANNEL.serverHandle(this).send(new LastEventPacket(pos, reason, particle.velocity, eventPosition, AcceleratorParticleLogic.getParticleBendDist(particle.lastBendDistance, particle.lastBendDistance2), activeItemParticle));
this.particle = null;
var renderedTrail = List.of(from, to);
NetworkContent.MACHINE_CHANNEL.serverHandle(this).send(new NetworkContent.AcceleratorParticleRenderPacket(pos, renderedTrail));
this.markDirty();
}
public void onParticleCollided(float relativeSpeed, Vec3d collision, BlockPos secondController, AcceleratorControllerBlockEntity secondControllerEntity) {
// create end portal area when two ender pearls collide, nether portal for two firecharges
if (relativeSpeed > Oritech.CONFIG.endPortalRequiredSpeed() && activeItemParticle.getItem().equals(Items.ENDER_PEARL) && secondControllerEntity.activeItemParticle.getItem().equals(Items.ENDER_PEARL)) {
spawnEndPortal(BlockPos.ofFloored(collision));
} else if (relativeSpeed > Oritech.CONFIG.netherPortalRequiredSpeed() && activeItemParticle.getItem().equals(Items.FIRE_CHARGE) && secondControllerEntity.activeItemParticle.getItem().equals(Items.FIRE_CHARGE)) {
spawnNetherPortal(BlockPos.ofFloored(collision));
} else {
var success = tryCraftResult(relativeSpeed, activeItemParticle, secondControllerEntity.activeItemParticle);
}
NetworkContent.MACHINE_CHANNEL.serverHandle(this).send(new LastEventPacket(pos, ParticleEvent.COLLIDED, relativeSpeed, BlockPos.ofFloored(particle.position), AcceleratorParticleLogic.getParticleBendDist(particle.lastBendDistance, particle.lastBendDistance2), activeItemParticle));
NetworkContent.MACHINE_CHANNEL.serverHandle(this).send(new LastEventPacket(secondController, ParticleEvent.COLLIDED, relativeSpeed, BlockPos.ofFloored(particle.position), AcceleratorParticleLogic.getParticleBendDist(particle.lastBendDistance, particle.lastBendDistance2), activeItemParticle));
this.removeParticleDueToCollision();
secondControllerEntity.removeParticleDueToCollision();
var particleCount = Math.pow(relativeSpeed, 0.5) / 2f + 1;
createCollisionParticles((int) relativeSpeed, collision, (int) particleCount);
ParticleContent.PARTICLE_COLLIDE.spawn(world, collision);
this.markDirty();
}
private void createCollisionParticles(int collisionEnergy, Vec3d collisionPosition, int shotCount) {
var energyMultiplier = 3 * Oritech.CONFIG.tachyonCollisionEnergyFactor();
int energyPotential = (int) (Math.pow(collisionEnergy / 2f, 2) * energyMultiplier * Oritech.CONFIG.accelerationRFCost()); // exactly N times the amount of energy used to accelerate
var energyPerRay = energyPotential / shotCount;
var rayRange = shotCount / 3;
var caughtParticles = 0;
for (int i = 0; i < shotCount; i++) {
var offset = VectorRandomUtils.getRandomOffset(world, collisionPosition, rayRange);
var direction = offset.subtract(collisionPosition).normalize();
var impactPos = BlackHoleBlockEntity.basicRaycast(collisionPosition.add(direction.multiply(1.2)), direction, rayRange, world);
if (impactPos != null) {
ParticleContent.BLACK_HOLE_EMISSION.spawn(world, collisionPosition, impactPos.toCenterPos());
// ParticleContent.DEBUG_BLOCK.spawn(world, Vec3d.of(impactPos));
var candidate = world.getBlockEntity(impactPos);
if (candidate instanceof ParticleCollectorBlockEntity collectorEntity) {
collectorEntity.onParticleCollided(energyPerRay);
caughtParticles++;
}
} else {
ParticleContent.BLACK_HOLE_EMISSION.spawn(world, collisionPosition, offset);
}
// System.out.println("caught: " + caughtParticles + " of " + shotCount);
}
}
private boolean tryCraftResult(float speed, ItemStack inputA, ItemStack inputB) {
if (inputA == null || inputA.isEmpty() || inputB == null || inputB.isEmpty()) return false;
var inputInv = new SimpleCraftingInventory(inputA, inputB);
var candidate = world.getRecipeManager().getFirstMatch(RecipeContent.PARTICLE_COLLISION, inputInv, world);
if (candidate.isEmpty()) {
// try again in different order
inputInv = new SimpleCraftingInventory(inputB, inputA);
candidate = world.getRecipeManager().getFirstMatch(RecipeContent.PARTICLE_COLLISION, inputInv, world);
}
if (candidate.isEmpty()) return false;
var recipe = candidate.get().value();
var requiredSpeed = recipe.getTime();
if (speed < requiredSpeed) return false;
var result = recipe.getResults();
if (inventory.heldStacks.get(1).getItem().equals(result.get(0).getItem())) {
inventory.heldStacks.get(1).increment(1);
} else {
inventory.setStack(1, result.get(0).copy());
}
return true;
}
private void spawnEndPortal(BlockPos pos) {
// create small end area around the portal
for (var candidate : BlockPos.iterateOutwards(pos, 8, 4, 8)) {
var dist = candidate.toCenterPos().distanceTo(pos.toCenterPos());
if (world.random.nextFloat() < dist / 8) continue;
var candidateState = world.getBlockState(candidate);
if (candidateState.isAir() || candidateState.isReplaceable() || candidateState.getBlock().getHardness() < 0)
continue;
if (!world.getBlockState(candidate.down()).getBlock().equals(Blocks.CHORUS_PLANT))
world.setBlockState(candidate, Blocks.END_STONE.getDefaultState());
// generate chorus flowers
if (world.random.nextFloat() > 0.8) {
var stateAbove = world.getBlockState(candidate.up());
if (stateAbove.isAir() || stateAbove.isReplaceable()) {
for (int i = 1; i < world.random.nextBetween(3, 6); i++) {
stateAbove = world.getBlockState(candidate.up(i));
if (stateAbove.isAir() || stateAbove.isReplaceable())
world.setBlockState(candidate.up(i), Blocks.CHORUS_PLANT.getDefaultState());
}
}
}
}
// create portal itself
world.setBlockState(pos, Blocks.END_PORTAL.getDefaultState());
world.setBlockState(pos.north(), Blocks.END_STONE.getDefaultState());
world.setBlockState(pos.east(), Blocks.END_STONE.getDefaultState());
world.setBlockState(pos.south(), Blocks.END_STONE.getDefaultState());
world.setBlockState(pos.west(), Blocks.END_STONE.getDefaultState());
}
private void spawnNetherPortal(BlockPos pos) {
// create small nether area around the portal
for (var candidate : BlockPos.iterateOutwards(pos, 12, 4, 12)) {
var dist = candidate.toCenterPos().distanceTo(pos.toCenterPos());
if (world.random.nextFloat() < dist / 12) continue;
var candidateState = world.getBlockState(candidate);
if (candidateState.isAir() || candidateState.isReplaceable() || candidateState.getBlock().getHardness() < 0)
continue;
world.setBlockState(candidate, Blocks.NETHERRACK.getDefaultState());
// generate fires
if (world.random.nextFloat() > 0.8) {
var stateAbove = world.getBlockState(candidate.up());
if (stateAbove.isAir() || stateAbove.isReplaceable()) {
world.setBlockState(candidate.up(), Blocks.FIRE.getDefaultState());
}
}
}
// spawn obsidian frame (3x4), with 2 portal blocks in the center
for (int x = 0; x < 3; x++) {
for (int y = 0; y < 4; y++) {
world.setBlockState(pos.add(x, y, 0), Blocks.OBSIDIAN.getDefaultState());
}
}
world.setBlockState(pos.add(1, 1, 0), Blocks.NETHER_PORTAL.getDefaultState());
world.setBlockState(pos.add(1, 2, 0), Blocks.NETHER_PORTAL.getDefaultState());
}
public void onParticleMoved(List<Vec3d> positions) {
if (positions.size() <= 1) return;
var resultList = new ArrayList<Vec3d>();
// deduplicate / shorten list
var positionSet = new HashSet<Vec3d>();
for (var position : positions) {
if (positionSet.contains(position)) {
// loop reached, stop the list
break;
}
positionSet.add(position);
resultList.add(position);
}
NetworkContent.MACHINE_CHANNEL.serverHandle(this).send(new NetworkContent.AcceleratorParticleRenderPacket(pos, resultList));
NetworkContent.MACHINE_CHANNEL.serverHandle(this).send(new LastEventPacket(pos, ParticleEvent.ACCELERATING, particle.velocity, BlockPos.ofFloored(particle.position), AcceleratorParticleLogic.getParticleBendDist(particle.lastBendDistance, particle.lastBendDistance2), activeItemParticle));
}
public AcceleratorParticleLogic.ActiveParticle getParticle() {
return particle;
}
public void onParticleInsertedClient() {
var soundPos = pos.toCenterPos();
world.playSound(soundPos.x, soundPos.y, soundPos.z, SoundContent.CABLE_MOVING, SoundCategory.BLOCKS, 1f, 1f, true);
}
public void onReceiveMovement(List<Vec3d> displayTrail) {
this.displayTrail = displayTrail;
if (displayTrail.size() < 2) return;
var playerPos = MinecraftClient.getInstance().player.getPos();
// play sound pos at closest segment
var minDist = Double.MAX_VALUE;
var soundPos = displayTrail.getFirst();
for (var candidate : displayTrail) {
var dist = candidate.distanceTo(playerPos);
if (dist < minDist) {
minDist = dist;
soundPos = candidate;
}
}
var pitch = Math.pow(lastEvent.lastEventSpeed, 0.1);
world.playSound(soundPos.x, soundPos.y, soundPos.z, SoundContent.PARTICLE_MOVING, SoundCategory.BLOCKS, 2f, (float) pitch, true);
}
// returns the amount of moment used
public float handleParticleEntityCollision(BlockPos checkPos, AcceleratorParticleLogic.ActiveParticle particle, float remainingMomentum, LivingEntity mob) {
var maxApplicableDamage = mob.getHealth();
var inflictedDamage = Math.min(remainingMomentum, maxApplicableDamage);
mob.damage(world.getDamageSources().magic(), remainingMomentum);
var position = mob.getBoundingBox().getCenter();
position = new Vec3d(position.x, particle.position.y, position.z);
ParticleContent.BIG_HIT.spawn(world, position);
return inflictedDamage;
}
public float handleParticleBlockCollision(BlockPos checkPos, AcceleratorParticleLogic.ActiveParticle particle, float remainingMomentum, BlockState hitState) {
var blockHardness = hitState.getHardness(world, checkPos);
// hit portal, create black hole with explosion
if (remainingMomentum > Oritech.CONFIG.blackHoleRequiredSpeed() && hitState.getBlock() instanceof Portal) {
createBlackHole(checkPos);
return remainingMomentum;
}
if (blockHardness < 0) // unbreakable block
return remainingMomentum;
if (remainingMomentum > blockHardness) {
world.addBlockBreakParticles(checkPos, hitState);
world.playSound(null, checkPos, hitState.getSoundGroup().getBreakSound(), SoundCategory.BLOCKS, 1f, 1f);
world.breakBlock(checkPos, true);
}
return blockHardness;
}
private void createBlackHole(BlockPos checkPos) {
ParticleContent.MELTDOWN_IMMINENT.spawn(world, checkPos.toCenterPos(), 30);
var center = checkPos.toCenterPos();
world.createExplosion(null, center.x, center.y, center.z, 10, false, World.ExplosionSourceType.BLOCK);
world.removeBlock(checkPos, false);
world.setBlockState(checkPos, BlockContent.BLACK_HOLE_BLOCK.getDefaultState());
}
public void handleParticleMotorInteraction(BlockPos motorBlock) {
var entity = world.getBlockEntity(motorBlock);
if (!(entity instanceof AcceleratorMotorBlockEntity motorEntity)) return;
var storage = motorEntity.getEnergyStorage(null);
var availableEnergy = storage.getAmount();
var speed = particle.velocity;
var cost = speed * Oritech.CONFIG.accelerationRFCost();
if (availableEnergy < cost) return;
storage.extract((long) cost, false);
storage.update();
particle.velocity += 1;
}
public void onReceivedEvent(LastEventPacket event) {
this.lastEvent = event;
var soundPos = event.lastEventPosition.toCenterPos();
if (event.lastEvent.equals(ParticleEvent.COLLIDED)) {
world.playSound(soundPos.x, soundPos.y, soundPos.z, SoundEvents.ENTITY_WARDEN_SONIC_BOOM, SoundCategory.BLOCKS, 5f, 1, true);
} else if (event.lastEvent.equals(ParticleEvent.EXITED_FAST) || event.lastEvent.equals(ParticleEvent.EXITED_NO_GATE)) {
world.playSound(soundPos.x, soundPos.y, soundPos.z, SoundEvents.ENTITY_WIND_CHARGE_WIND_BURST.value(), SoundCategory.BLOCKS, 3f, 1, true);
}
}
@Override
public ItemApi.InventoryStorage getInventoryStorage(Direction direction) {
return inventory;
}
@Override
public void saveExtraData(PacketByteBuf buf) {
buf.writeBlockPos(pos);
}
@Override
public Text getDisplayName() {
return Text.literal("");
}
@Nullable
@Override
public ScreenHandler createMenu(int syncId, PlayerInventory playerInventory, PlayerEntity player) {
return new AcceleratorScreenHandler(syncId, playerInventory, this);
}
@Override
public List<GuiSlot> getGuiSlots() {
return List.of(new GuiSlot(0, 7, 10),
new GuiSlot(1, 7, 60, true));
}
@Override
public boolean showEnergy() {
return false;
}
@Override
public float getDisplayedEnergyUsage() {
return 0;
}
@Override
public float getProgress() {
return 0;
}
@Override
public InventoryInputMode getInventoryInputMode() {
return InventoryInputMode.FILL_LEFT_TO_RIGHT;
}
@Override
public Inventory getDisplayedInventory() {
return inventory;
}
@Override
public ScreenHandlerType<?> getScreenHandlerType() {
return ModScreens.ACCELERATOR_SCREEN;
}
@Override
public boolean inputOptionsEnabled() {
return false;
}
@Override
public boolean showProgress() {
return false;
}
public record LastEventPacket(BlockPos position,
ParticleEvent lastEvent,
// for no gate found events, we can calculate the acceptable dist based on speed
float lastEventSpeed,
// this is particle speed usually, and collision speed for collisions
BlockPos lastEventPosition, // where it collided/exited
float minBendDist, // acceptable dist can be calculated from dist
ItemStack activeParticle
) {
}
public enum ParticleEvent {
IDLE, // nothing was insert yet
ERROR, // no ring was found
ACCELERATING, // particle is in collider
COLLIDED,
EXITED_FAST, // particle was too fast to take curve
EXITED_NO_GATE // no gate found in range
}
}