333 lines
14 KiB
Java
333 lines
14 KiB
Java
package net.minecraft.world.level.block.entity.vault;
|
|
|
|
import com.google.common.annotations.VisibleForTesting;
|
|
import com.mojang.serialization.DynamicOps;
|
|
import java.util.List;
|
|
import java.util.Set;
|
|
import java.util.UUID;
|
|
import net.minecraft.Util;
|
|
import net.minecraft.core.BlockPos;
|
|
import net.minecraft.core.Direction;
|
|
import net.minecraft.core.HolderLookup;
|
|
import net.minecraft.core.particles.ParticleOptions;
|
|
import net.minecraft.core.particles.ParticleTypes;
|
|
import net.minecraft.nbt.CompoundTag;
|
|
import net.minecraft.nbt.NbtOps;
|
|
import net.minecraft.nbt.Tag;
|
|
import net.minecraft.network.protocol.Packet;
|
|
import net.minecraft.network.protocol.game.ClientGamePacketListener;
|
|
import net.minecraft.network.protocol.game.ClientboundBlockEntityDataPacket;
|
|
import net.minecraft.resources.RegistryOps;
|
|
import net.minecraft.resources.ResourceKey;
|
|
import net.minecraft.server.level.ServerLevel;
|
|
import net.minecraft.sounds.SoundEvent;
|
|
import net.minecraft.sounds.SoundEvents;
|
|
import net.minecraft.sounds.SoundSource;
|
|
import net.minecraft.stats.Stats;
|
|
import net.minecraft.util.Mth;
|
|
import net.minecraft.util.RandomSource;
|
|
import net.minecraft.world.entity.player.Player;
|
|
import net.minecraft.world.item.ItemStack;
|
|
import net.minecraft.world.level.Level;
|
|
import net.minecraft.world.level.block.VaultBlock;
|
|
import net.minecraft.world.level.block.entity.BlockEntity;
|
|
import net.minecraft.world.level.block.entity.BlockEntityType;
|
|
import net.minecraft.world.level.block.state.BlockState;
|
|
import net.minecraft.world.level.storage.loot.LootParams;
|
|
import net.minecraft.world.level.storage.loot.LootTable;
|
|
import net.minecraft.world.level.storage.loot.LootParams.Builder;
|
|
import net.minecraft.world.level.storage.loot.parameters.LootContextParamSets;
|
|
import net.minecraft.world.level.storage.loot.parameters.LootContextParams;
|
|
import net.minecraft.world.phys.Vec3;
|
|
import org.jetbrains.annotations.Nullable;
|
|
|
|
public class VaultBlockEntity extends BlockEntity {
|
|
private final VaultServerData serverData = new VaultServerData();
|
|
private final VaultSharedData sharedData = new VaultSharedData();
|
|
private final VaultClientData clientData = new VaultClientData();
|
|
private VaultConfig config = VaultConfig.DEFAULT;
|
|
|
|
public VaultBlockEntity(BlockPos pos, BlockState state) {
|
|
super(BlockEntityType.VAULT, pos, state);
|
|
}
|
|
|
|
@Nullable
|
|
@Override
|
|
public Packet<ClientGamePacketListener> getUpdatePacket() {
|
|
return ClientboundBlockEntityDataPacket.create(this);
|
|
}
|
|
|
|
@Override
|
|
public CompoundTag getUpdateTag(HolderLookup.Provider registries) {
|
|
return Util.make(
|
|
new CompoundTag(),
|
|
compoundTag -> compoundTag.store("shared_data", VaultSharedData.CODEC, registries.createSerializationContext(NbtOps.INSTANCE), this.sharedData)
|
|
);
|
|
}
|
|
|
|
@Override
|
|
protected void saveAdditional(CompoundTag tag, HolderLookup.Provider registries) {
|
|
super.saveAdditional(tag, registries);
|
|
RegistryOps<Tag> registryOps = registries.createSerializationContext(NbtOps.INSTANCE);
|
|
tag.store("config", VaultConfig.CODEC, registryOps, this.config);
|
|
tag.store("shared_data", VaultSharedData.CODEC, registryOps, this.sharedData);
|
|
tag.store("server_data", VaultServerData.CODEC, registryOps, this.serverData);
|
|
}
|
|
|
|
@Override
|
|
protected void loadAdditional(CompoundTag tag, HolderLookup.Provider registries) {
|
|
super.loadAdditional(tag, registries);
|
|
DynamicOps<Tag> dynamicOps = registries.createSerializationContext(NbtOps.INSTANCE);
|
|
tag.read("server_data", VaultServerData.CODEC, dynamicOps).ifPresent(this.serverData::set);
|
|
this.config = (VaultConfig)tag.read("config", VaultConfig.CODEC, dynamicOps).orElse(VaultConfig.DEFAULT);
|
|
tag.read("shared_data", VaultSharedData.CODEC, dynamicOps).ifPresent(this.sharedData::set);
|
|
}
|
|
|
|
@Nullable
|
|
public VaultServerData getServerData() {
|
|
return this.level != null && !this.level.isClientSide ? this.serverData : null;
|
|
}
|
|
|
|
public VaultSharedData getSharedData() {
|
|
return this.sharedData;
|
|
}
|
|
|
|
public VaultClientData getClientData() {
|
|
return this.clientData;
|
|
}
|
|
|
|
public VaultConfig getConfig() {
|
|
return this.config;
|
|
}
|
|
|
|
@VisibleForTesting
|
|
public void setConfig(VaultConfig config) {
|
|
this.config = config;
|
|
}
|
|
|
|
public static final class Client {
|
|
private static final int PARTICLE_TICK_RATE = 20;
|
|
private static final float IDLE_PARTICLE_CHANCE = 0.5F;
|
|
private static final float AMBIENT_SOUND_CHANCE = 0.02F;
|
|
private static final int ACTIVATION_PARTICLE_COUNT = 20;
|
|
private static final int DEACTIVATION_PARTICLE_COUNT = 20;
|
|
|
|
public static void tick(Level level, BlockPos pos, BlockState state, VaultClientData clientData, VaultSharedData sharedData) {
|
|
clientData.updateDisplayItemSpin();
|
|
if (level.getGameTime() % 20L == 0L) {
|
|
emitConnectionParticlesForNearbyPlayers(level, pos, state, sharedData);
|
|
}
|
|
|
|
emitIdleParticles(level, pos, sharedData, state.getValue(VaultBlock.OMINOUS) ? ParticleTypes.SOUL_FIRE_FLAME : ParticleTypes.SMALL_FLAME);
|
|
playIdleSounds(level, pos, sharedData);
|
|
}
|
|
|
|
public static void emitActivationParticles(Level level, BlockPos pos, BlockState state, VaultSharedData sharedData, ParticleOptions particle) {
|
|
emitConnectionParticlesForNearbyPlayers(level, pos, state, sharedData);
|
|
RandomSource randomSource = level.random;
|
|
|
|
for (int i = 0; i < 20; i++) {
|
|
Vec3 vec3 = randomPosInsideCage(pos, randomSource);
|
|
level.addParticle(ParticleTypes.SMOKE, vec3.x(), vec3.y(), vec3.z(), 0.0, 0.0, 0.0);
|
|
level.addParticle(particle, vec3.x(), vec3.y(), vec3.z(), 0.0, 0.0, 0.0);
|
|
}
|
|
}
|
|
|
|
public static void emitDeactivationParticles(Level level, BlockPos pos, ParticleOptions particle) {
|
|
RandomSource randomSource = level.random;
|
|
|
|
for (int i = 0; i < 20; i++) {
|
|
Vec3 vec3 = randomPosCenterOfCage(pos, randomSource);
|
|
Vec3 vec32 = new Vec3(randomSource.nextGaussian() * 0.02, randomSource.nextGaussian() * 0.02, randomSource.nextGaussian() * 0.02);
|
|
level.addParticle(particle, vec3.x(), vec3.y(), vec3.z(), vec32.x(), vec32.y(), vec32.z());
|
|
}
|
|
}
|
|
|
|
private static void emitIdleParticles(Level level, BlockPos pos, VaultSharedData sharedData, ParticleOptions particle) {
|
|
RandomSource randomSource = level.getRandom();
|
|
if (randomSource.nextFloat() <= 0.5F) {
|
|
Vec3 vec3 = randomPosInsideCage(pos, randomSource);
|
|
level.addParticle(ParticleTypes.SMOKE, vec3.x(), vec3.y(), vec3.z(), 0.0, 0.0, 0.0);
|
|
if (shouldDisplayActiveEffects(sharedData)) {
|
|
level.addParticle(particle, vec3.x(), vec3.y(), vec3.z(), 0.0, 0.0, 0.0);
|
|
}
|
|
}
|
|
}
|
|
|
|
private static void emitConnectionParticlesForPlayer(Level level, Vec3 pos, Player player) {
|
|
RandomSource randomSource = level.random;
|
|
Vec3 vec3 = pos.vectorTo(player.position().add(0.0, player.getBbHeight() / 2.0F, 0.0));
|
|
int i = Mth.nextInt(randomSource, 2, 5);
|
|
|
|
for (int j = 0; j < i; j++) {
|
|
Vec3 vec32 = vec3.offsetRandom(randomSource, 1.0F);
|
|
level.addParticle(ParticleTypes.VAULT_CONNECTION, pos.x(), pos.y(), pos.z(), vec32.x(), vec32.y(), vec32.z());
|
|
}
|
|
}
|
|
|
|
private static void emitConnectionParticlesForNearbyPlayers(Level level, BlockPos pos, BlockState state, VaultSharedData sharedData) {
|
|
Set<UUID> set = sharedData.getConnectedPlayers();
|
|
if (!set.isEmpty()) {
|
|
Vec3 vec3 = keyholePos(pos, state.getValue(VaultBlock.FACING));
|
|
|
|
for (UUID uUID : set) {
|
|
Player player = level.getPlayerByUUID(uUID);
|
|
if (player != null && isWithinConnectionRange(pos, sharedData, player)) {
|
|
emitConnectionParticlesForPlayer(level, vec3, player);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
private static boolean isWithinConnectionRange(BlockPos pos, VaultSharedData sharedData, Player player) {
|
|
return player.blockPosition().distSqr(pos) <= Mth.square(sharedData.connectedParticlesRange());
|
|
}
|
|
|
|
private static void playIdleSounds(Level level, BlockPos pos, VaultSharedData sharedData) {
|
|
if (shouldDisplayActiveEffects(sharedData)) {
|
|
RandomSource randomSource = level.getRandom();
|
|
if (randomSource.nextFloat() <= 0.02F) {
|
|
level.playLocalSound(pos, SoundEvents.VAULT_AMBIENT, SoundSource.BLOCKS, randomSource.nextFloat() * 0.25F + 0.75F, randomSource.nextFloat() + 0.5F, false);
|
|
}
|
|
}
|
|
}
|
|
|
|
public static boolean shouldDisplayActiveEffects(VaultSharedData sharedData) {
|
|
return sharedData.hasDisplayItem();
|
|
}
|
|
|
|
private static Vec3 randomPosCenterOfCage(BlockPos pos, RandomSource random) {
|
|
return Vec3.atLowerCornerOf(pos).add(Mth.nextDouble(random, 0.4, 0.6), Mth.nextDouble(random, 0.4, 0.6), Mth.nextDouble(random, 0.4, 0.6));
|
|
}
|
|
|
|
private static Vec3 randomPosInsideCage(BlockPos pos, RandomSource random) {
|
|
return Vec3.atLowerCornerOf(pos).add(Mth.nextDouble(random, 0.1, 0.9), Mth.nextDouble(random, 0.25, 0.75), Mth.nextDouble(random, 0.1, 0.9));
|
|
}
|
|
|
|
private static Vec3 keyholePos(BlockPos pos, Direction facing) {
|
|
return Vec3.atBottomCenterOf(pos).add(facing.getStepX() * 0.5, 1.75, facing.getStepZ() * 0.5);
|
|
}
|
|
}
|
|
|
|
public static final class Server {
|
|
private static final int UNLOCKING_DELAY_TICKS = 14;
|
|
private static final int DISPLAY_CYCLE_TICK_RATE = 20;
|
|
private static final int INSERT_FAIL_SOUND_BUFFER_TICKS = 15;
|
|
|
|
public static void tick(ServerLevel level, BlockPos pos, BlockState state, VaultConfig config, VaultServerData serverData, VaultSharedData sharedData) {
|
|
VaultState vaultState = state.getValue(VaultBlock.STATE);
|
|
if (shouldCycleDisplayItem(level.getGameTime(), vaultState)) {
|
|
cycleDisplayItemFromLootTable(level, vaultState, config, sharedData, pos);
|
|
}
|
|
|
|
BlockState blockState = state;
|
|
if (level.getGameTime() >= serverData.stateUpdatingResumesAt()) {
|
|
blockState = state.setValue(VaultBlock.STATE, vaultState.tickAndGetNext(level, pos, config, serverData, sharedData));
|
|
if (state != blockState) {
|
|
setVaultState(level, pos, state, blockState, config, sharedData);
|
|
}
|
|
}
|
|
|
|
if (serverData.isDirty || sharedData.isDirty) {
|
|
VaultBlockEntity.setChanged(level, pos, state);
|
|
if (sharedData.isDirty) {
|
|
level.sendBlockUpdated(pos, state, blockState, 2);
|
|
}
|
|
|
|
serverData.isDirty = false;
|
|
sharedData.isDirty = false;
|
|
}
|
|
}
|
|
|
|
public static void tryInsertKey(
|
|
ServerLevel level,
|
|
BlockPos pos,
|
|
BlockState state,
|
|
VaultConfig config,
|
|
VaultServerData serverData,
|
|
VaultSharedData sharedData,
|
|
Player player,
|
|
ItemStack stack
|
|
) {
|
|
VaultState vaultState = state.getValue(VaultBlock.STATE);
|
|
if (canEjectReward(config, vaultState)) {
|
|
if (!isValidToInsert(config, stack)) {
|
|
playInsertFailSound(level, serverData, pos, SoundEvents.VAULT_INSERT_ITEM_FAIL);
|
|
} else if (serverData.hasRewardedPlayer(player)) {
|
|
playInsertFailSound(level, serverData, pos, SoundEvents.VAULT_REJECT_REWARDED_PLAYER);
|
|
} else {
|
|
List<ItemStack> list = resolveItemsToEject(level, config, pos, player, stack);
|
|
if (!list.isEmpty()) {
|
|
player.awardStat(Stats.ITEM_USED.get(stack.getItem()));
|
|
stack.consume(config.keyItem().getCount(), player);
|
|
unlock(level, state, pos, config, serverData, sharedData, list);
|
|
serverData.addToRewardedPlayers(player);
|
|
sharedData.updateConnectedPlayersWithinRange(level, pos, serverData, config, config.deactivationRange());
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
static void setVaultState(ServerLevel level, BlockPos pos, BlockState oldState, BlockState newState, VaultConfig config, VaultSharedData sharedData) {
|
|
VaultState vaultState = oldState.getValue(VaultBlock.STATE);
|
|
VaultState vaultState2 = newState.getValue(VaultBlock.STATE);
|
|
level.setBlock(pos, newState, 3);
|
|
vaultState.onTransition(level, pos, vaultState2, config, sharedData, (Boolean)newState.getValue(VaultBlock.OMINOUS));
|
|
}
|
|
|
|
static void cycleDisplayItemFromLootTable(ServerLevel level, VaultState state, VaultConfig config, VaultSharedData sharedData, BlockPos pos) {
|
|
if (!canEjectReward(config, state)) {
|
|
sharedData.setDisplayItem(ItemStack.EMPTY);
|
|
} else {
|
|
ItemStack itemStack = getRandomDisplayItemFromLootTable(level, pos, (ResourceKey<LootTable>)config.overrideLootTableToDisplay().orElse(config.lootTable()));
|
|
sharedData.setDisplayItem(itemStack);
|
|
}
|
|
}
|
|
|
|
private static ItemStack getRandomDisplayItemFromLootTable(ServerLevel level, BlockPos pos, ResourceKey<LootTable> lootTable) {
|
|
LootTable lootTable2 = level.getServer().reloadableRegistries().getLootTable(lootTable);
|
|
LootParams lootParams = new Builder(level).withParameter(LootContextParams.ORIGIN, Vec3.atCenterOf(pos)).create(LootContextParamSets.VAULT);
|
|
List<ItemStack> list = lootTable2.getRandomItems(lootParams, level.getRandom());
|
|
return list.isEmpty() ? ItemStack.EMPTY : Util.getRandom(list, level.getRandom());
|
|
}
|
|
|
|
private static void unlock(
|
|
ServerLevel level, BlockState state, BlockPos pos, VaultConfig config, VaultServerData serverData, VaultSharedData sharedData, List<ItemStack> itemsToEject
|
|
) {
|
|
serverData.setItemsToEject(itemsToEject);
|
|
sharedData.setDisplayItem(serverData.getNextItemToEject());
|
|
serverData.pauseStateUpdatingUntil(level.getGameTime() + 14L);
|
|
setVaultState(level, pos, state, state.setValue(VaultBlock.STATE, VaultState.UNLOCKING), config, sharedData);
|
|
}
|
|
|
|
private static List<ItemStack> resolveItemsToEject(ServerLevel level, VaultConfig config, BlockPos pos, Player player, ItemStack key) {
|
|
LootTable lootTable = level.getServer().reloadableRegistries().getLootTable(config.lootTable());
|
|
LootParams lootParams = new Builder(level)
|
|
.withParameter(LootContextParams.ORIGIN, Vec3.atCenterOf(pos))
|
|
.withLuck(player.getLuck())
|
|
.withParameter(LootContextParams.THIS_ENTITY, player)
|
|
.withParameter(LootContextParams.TOOL, key)
|
|
.create(LootContextParamSets.VAULT);
|
|
return lootTable.getRandomItems(lootParams);
|
|
}
|
|
|
|
private static boolean canEjectReward(VaultConfig config, VaultState state) {
|
|
return !config.keyItem().isEmpty() && state != VaultState.INACTIVE;
|
|
}
|
|
|
|
private static boolean isValidToInsert(VaultConfig config, ItemStack stack) {
|
|
return ItemStack.isSameItemSameComponents(stack, config.keyItem()) && stack.getCount() >= config.keyItem().getCount();
|
|
}
|
|
|
|
private static boolean shouldCycleDisplayItem(long gameTime, VaultState state) {
|
|
return gameTime % 20L == 0L && state == VaultState.ACTIVE;
|
|
}
|
|
|
|
private static void playInsertFailSound(ServerLevel level, VaultServerData serverData, BlockPos pos, SoundEvent sound) {
|
|
if (level.getGameTime() >= serverData.getLastInsertFailTimestamp() + 15L) {
|
|
level.playSound(null, pos, sound, SoundSource.BLOCKS);
|
|
serverData.setLastInsertFailTimestamp(level.getGameTime());
|
|
}
|
|
}
|
|
}
|
|
}
|