414 lines
16 KiB
Java
414 lines
16 KiB
Java
package net.minecraft.world.level.block.entity.trialspawner;
|
|
|
|
import com.google.common.annotations.VisibleForTesting;
|
|
import com.mojang.serialization.Codec;
|
|
import com.mojang.serialization.MapCodec;
|
|
import com.mojang.serialization.codecs.RecordCodecBuilder;
|
|
import it.unimi.dsi.fastutil.objects.ObjectArrayList;
|
|
import java.util.Optional;
|
|
import java.util.UUID;
|
|
import net.minecraft.core.BlockPos;
|
|
import net.minecraft.core.Direction;
|
|
import net.minecraft.core.Holder;
|
|
import net.minecraft.core.dispenser.DefaultDispenseItemBehavior;
|
|
import net.minecraft.core.particles.ParticleOptions;
|
|
import net.minecraft.core.particles.ParticleTypes;
|
|
import net.minecraft.core.particles.SimpleParticleType;
|
|
import net.minecraft.nbt.CompoundTag;
|
|
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.util.Mth;
|
|
import net.minecraft.util.RandomSource;
|
|
import net.minecraft.world.Difficulty;
|
|
import net.minecraft.world.entity.Entity;
|
|
import net.minecraft.world.entity.EntitySpawnReason;
|
|
import net.minecraft.world.entity.EntityType;
|
|
import net.minecraft.world.entity.Mob;
|
|
import net.minecraft.world.entity.SpawnPlacements;
|
|
import net.minecraft.world.item.ItemStack;
|
|
import net.minecraft.world.level.ClipContext;
|
|
import net.minecraft.world.level.GameRules;
|
|
import net.minecraft.world.level.Level;
|
|
import net.minecraft.world.level.SpawnData;
|
|
import net.minecraft.world.level.ClipContext.Block;
|
|
import net.minecraft.world.level.ClipContext.Fluid;
|
|
import net.minecraft.world.level.block.TrialSpawnerBlock;
|
|
import net.minecraft.world.level.gameevent.GameEvent;
|
|
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.phys.BlockHitResult;
|
|
import net.minecraft.world.phys.Vec3;
|
|
import net.minecraft.world.phys.HitResult.Type;
|
|
import net.minecraft.world.phys.shapes.CollisionContext;
|
|
|
|
public final class TrialSpawner {
|
|
public static final String NORMAL_CONFIG_TAG_NAME = "normal_config";
|
|
public static final String OMINOUS_CONFIG_TAG_NAME = "ominous_config";
|
|
public static final int DETECT_PLAYER_SPAWN_BUFFER = 40;
|
|
private static final int DEFAULT_TARGET_COOLDOWN_LENGTH = 36000;
|
|
private static final int DEFAULT_PLAYER_SCAN_RANGE = 14;
|
|
private static final int MAX_MOB_TRACKING_DISTANCE = 47;
|
|
private static final int MAX_MOB_TRACKING_DISTANCE_SQR = Mth.square(47);
|
|
private static final float SPAWNING_AMBIENT_SOUND_CHANCE = 0.02F;
|
|
private Holder<TrialSpawnerConfig> normalConfig;
|
|
private Holder<TrialSpawnerConfig> ominousConfig;
|
|
private final TrialSpawnerData data;
|
|
private final int requiredPlayerRange;
|
|
private final int targetCooldownLength;
|
|
private final TrialSpawner.StateAccessor stateAccessor;
|
|
private PlayerDetector playerDetector;
|
|
private final PlayerDetector.EntitySelector entitySelector;
|
|
private boolean overridePeacefulAndMobSpawnRule;
|
|
private boolean isOminous;
|
|
|
|
public MapCodec<TrialSpawner> codec() {
|
|
return RecordCodecBuilder.mapCodec(
|
|
instance -> instance.group(
|
|
TrialSpawnerConfig.CODEC.optionalFieldOf("normal_config", Holder.direct(TrialSpawnerConfig.DEFAULT)).forGetter(trialSpawner -> trialSpawner.normalConfig),
|
|
TrialSpawnerConfig.CODEC
|
|
.optionalFieldOf("ominous_config", Holder.direct(TrialSpawnerConfig.DEFAULT))
|
|
.forGetter(trialSpawner -> trialSpawner.ominousConfig),
|
|
TrialSpawnerData.MAP_CODEC.forGetter(TrialSpawner::getData),
|
|
Codec.intRange(0, Integer.MAX_VALUE).optionalFieldOf("target_cooldown_length", 36000).forGetter(TrialSpawner::getTargetCooldownLength),
|
|
Codec.intRange(1, 128).optionalFieldOf("required_player_range", 14).forGetter(TrialSpawner::getRequiredPlayerRange)
|
|
)
|
|
.apply(
|
|
instance,
|
|
(holder, holder2, trialSpawnerData, integer, integer2) -> new TrialSpawner(
|
|
holder, holder2, trialSpawnerData, integer, integer2, this.stateAccessor, this.playerDetector, this.entitySelector
|
|
)
|
|
)
|
|
);
|
|
}
|
|
|
|
public TrialSpawner(TrialSpawner.StateAccessor stateAccessor, PlayerDetector playerDetector, PlayerDetector.EntitySelector entitySelector) {
|
|
this(
|
|
Holder.direct(TrialSpawnerConfig.DEFAULT),
|
|
Holder.direct(TrialSpawnerConfig.DEFAULT),
|
|
new TrialSpawnerData(),
|
|
36000,
|
|
14,
|
|
stateAccessor,
|
|
playerDetector,
|
|
entitySelector
|
|
);
|
|
}
|
|
|
|
public TrialSpawner(
|
|
Holder<TrialSpawnerConfig> normalConfig,
|
|
Holder<TrialSpawnerConfig> ominousConfig,
|
|
TrialSpawnerData data,
|
|
int targetCooldownLength,
|
|
int requiredPlayerRange,
|
|
TrialSpawner.StateAccessor stateAccessor,
|
|
PlayerDetector playerDetector,
|
|
PlayerDetector.EntitySelector entitySelector
|
|
) {
|
|
this.normalConfig = normalConfig;
|
|
this.ominousConfig = ominousConfig;
|
|
this.data = data;
|
|
this.targetCooldownLength = targetCooldownLength;
|
|
this.requiredPlayerRange = requiredPlayerRange;
|
|
this.stateAccessor = stateAccessor;
|
|
this.playerDetector = playerDetector;
|
|
this.entitySelector = entitySelector;
|
|
}
|
|
|
|
public TrialSpawnerConfig getConfig() {
|
|
return this.isOminous ? this.getOminousConfig() : this.getNormalConfig();
|
|
}
|
|
|
|
@VisibleForTesting
|
|
public TrialSpawnerConfig getNormalConfig() {
|
|
return this.normalConfig.value();
|
|
}
|
|
|
|
@VisibleForTesting
|
|
public TrialSpawnerConfig getOminousConfig() {
|
|
return this.ominousConfig.value();
|
|
}
|
|
|
|
public void applyOminous(ServerLevel level, BlockPos pos) {
|
|
level.setBlock(pos, level.getBlockState(pos).setValue(TrialSpawnerBlock.OMINOUS, true), 3);
|
|
level.levelEvent(3020, pos, 1);
|
|
this.isOminous = true;
|
|
this.data.resetAfterBecomingOminous(this, level);
|
|
}
|
|
|
|
public void removeOminous(ServerLevel level, BlockPos pos) {
|
|
level.setBlock(pos, level.getBlockState(pos).setValue(TrialSpawnerBlock.OMINOUS, false), 3);
|
|
this.isOminous = false;
|
|
}
|
|
|
|
public boolean isOminous() {
|
|
return this.isOminous;
|
|
}
|
|
|
|
public TrialSpawnerData getData() {
|
|
return this.data;
|
|
}
|
|
|
|
public int getTargetCooldownLength() {
|
|
return this.targetCooldownLength;
|
|
}
|
|
|
|
public int getRequiredPlayerRange() {
|
|
return this.requiredPlayerRange;
|
|
}
|
|
|
|
public TrialSpawnerState getState() {
|
|
return this.stateAccessor.getState();
|
|
}
|
|
|
|
public void setState(Level level, TrialSpawnerState state) {
|
|
this.stateAccessor.setState(level, state);
|
|
}
|
|
|
|
public void markUpdated() {
|
|
this.stateAccessor.markUpdated();
|
|
}
|
|
|
|
public PlayerDetector getPlayerDetector() {
|
|
return this.playerDetector;
|
|
}
|
|
|
|
public PlayerDetector.EntitySelector getEntitySelector() {
|
|
return this.entitySelector;
|
|
}
|
|
|
|
public boolean canSpawnInLevel(ServerLevel level) {
|
|
if (this.overridePeacefulAndMobSpawnRule) {
|
|
return true;
|
|
} else {
|
|
return level.getDifficulty() == Difficulty.PEACEFUL ? false : level.getGameRules().getBoolean(GameRules.RULE_DOMOBSPAWNING);
|
|
}
|
|
}
|
|
|
|
public Optional<UUID> spawnMob(ServerLevel level, BlockPos pos) {
|
|
RandomSource randomSource = level.getRandom();
|
|
SpawnData spawnData = this.data.getOrCreateNextSpawnData(this, level.getRandom());
|
|
CompoundTag compoundTag = spawnData.entityToSpawn();
|
|
Optional<EntityType<?>> optional = EntityType.by(compoundTag);
|
|
if (optional.isEmpty()) {
|
|
return Optional.empty();
|
|
} else {
|
|
Vec3 vec3 = (Vec3)compoundTag.read("Pos", Vec3.CODEC)
|
|
.orElseGet(
|
|
() -> new Vec3(
|
|
pos.getX() + (randomSource.nextDouble() - randomSource.nextDouble()) * this.getConfig().spawnRange() + 0.5,
|
|
pos.getY() + randomSource.nextInt(3) - 1,
|
|
pos.getZ() + (randomSource.nextDouble() - randomSource.nextDouble()) * this.getConfig().spawnRange() + 0.5
|
|
)
|
|
);
|
|
if (!level.noCollision(((EntityType)optional.get()).getSpawnAABB(vec3.x, vec3.y, vec3.z))) {
|
|
return Optional.empty();
|
|
} else if (!inLineOfSight(level, pos.getCenter(), vec3)) {
|
|
return Optional.empty();
|
|
} else {
|
|
BlockPos blockPos = BlockPos.containing(vec3);
|
|
if (!SpawnPlacements.checkSpawnRules((EntityType)optional.get(), level, EntitySpawnReason.TRIAL_SPAWNER, blockPos, level.getRandom())) {
|
|
return Optional.empty();
|
|
} else {
|
|
if (spawnData.getCustomSpawnRules().isPresent()) {
|
|
SpawnData.CustomSpawnRules customSpawnRules = (SpawnData.CustomSpawnRules)spawnData.getCustomSpawnRules().get();
|
|
if (!customSpawnRules.isValidPosition(blockPos, level)) {
|
|
return Optional.empty();
|
|
}
|
|
}
|
|
|
|
Entity entity = EntityType.loadEntityRecursive(compoundTag, level, EntitySpawnReason.TRIAL_SPAWNER, entityx -> {
|
|
entityx.snapTo(vec3.x, vec3.y, vec3.z, randomSource.nextFloat() * 360.0F, 0.0F);
|
|
return entityx;
|
|
});
|
|
if (entity == null) {
|
|
return Optional.empty();
|
|
} else {
|
|
if (entity instanceof Mob mob) {
|
|
if (!mob.checkSpawnObstruction(level)) {
|
|
return Optional.empty();
|
|
}
|
|
|
|
boolean bl = spawnData.getEntityToSpawn().size() == 1 && spawnData.getEntityToSpawn().getString("id").isPresent();
|
|
if (bl) {
|
|
mob.finalizeSpawn(level, level.getCurrentDifficultyAt(mob.blockPosition()), EntitySpawnReason.TRIAL_SPAWNER, null);
|
|
}
|
|
|
|
mob.setPersistenceRequired();
|
|
spawnData.getEquipment().ifPresent(mob::equip);
|
|
}
|
|
|
|
if (!level.tryAddFreshEntityWithPassengers(entity)) {
|
|
return Optional.empty();
|
|
} else {
|
|
TrialSpawner.FlameParticle flameParticle = this.isOminous ? TrialSpawner.FlameParticle.OMINOUS : TrialSpawner.FlameParticle.NORMAL;
|
|
level.levelEvent(3011, pos, flameParticle.encode());
|
|
level.levelEvent(3012, blockPos, flameParticle.encode());
|
|
level.gameEvent(entity, GameEvent.ENTITY_PLACE, blockPos);
|
|
return Optional.of(entity.getUUID());
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
public void ejectReward(ServerLevel level, BlockPos pos, ResourceKey<LootTable> lootTable) {
|
|
LootTable lootTable2 = level.getServer().reloadableRegistries().getLootTable(lootTable);
|
|
LootParams lootParams = new Builder(level).create(LootContextParamSets.EMPTY);
|
|
ObjectArrayList<ItemStack> objectArrayList = lootTable2.getRandomItems(lootParams);
|
|
if (!objectArrayList.isEmpty()) {
|
|
for (ItemStack itemStack : objectArrayList) {
|
|
DefaultDispenseItemBehavior.spawnItem(level, itemStack, 2, Direction.UP, Vec3.atBottomCenterOf(pos).relative(Direction.UP, 1.2));
|
|
}
|
|
|
|
level.levelEvent(3014, pos, 0);
|
|
}
|
|
}
|
|
|
|
public void tickClient(Level level, BlockPos pos, boolean isOminous) {
|
|
TrialSpawnerState trialSpawnerState = this.getState();
|
|
trialSpawnerState.emitParticles(level, pos, isOminous);
|
|
if (trialSpawnerState.hasSpinningMob()) {
|
|
double d = Math.max(0L, this.data.nextMobSpawnsAt - level.getGameTime());
|
|
this.data.oSpin = this.data.spin;
|
|
this.data.spin = (this.data.spin + trialSpawnerState.spinningMobSpeed() / (d + 200.0)) % 360.0;
|
|
}
|
|
|
|
if (trialSpawnerState.isCapableOfSpawning()) {
|
|
RandomSource randomSource = level.getRandom();
|
|
if (randomSource.nextFloat() <= 0.02F) {
|
|
SoundEvent soundEvent = isOminous ? SoundEvents.TRIAL_SPAWNER_AMBIENT_OMINOUS : SoundEvents.TRIAL_SPAWNER_AMBIENT;
|
|
level.playLocalSound(pos, soundEvent, SoundSource.BLOCKS, randomSource.nextFloat() * 0.25F + 0.75F, randomSource.nextFloat() + 0.5F, false);
|
|
}
|
|
}
|
|
}
|
|
|
|
public void tickServer(ServerLevel level, BlockPos pos, boolean isOminous) {
|
|
this.isOminous = isOminous;
|
|
TrialSpawnerState trialSpawnerState = this.getState();
|
|
if (this.data.currentMobs.removeIf(uUID -> shouldMobBeUntracked(level, pos, uUID))) {
|
|
this.data.nextMobSpawnsAt = level.getGameTime() + this.getConfig().ticksBetweenSpawn();
|
|
}
|
|
|
|
TrialSpawnerState trialSpawnerState2 = trialSpawnerState.tickAndGetNext(pos, this, level);
|
|
if (trialSpawnerState2 != trialSpawnerState) {
|
|
this.setState(level, trialSpawnerState2);
|
|
}
|
|
}
|
|
|
|
private static boolean shouldMobBeUntracked(ServerLevel level, BlockPos pos, UUID uuid) {
|
|
Entity entity = level.getEntity(uuid);
|
|
return entity == null
|
|
|| !entity.isAlive()
|
|
|| !entity.level().dimension().equals(level.dimension())
|
|
|| entity.blockPosition().distSqr(pos) > MAX_MOB_TRACKING_DISTANCE_SQR;
|
|
}
|
|
|
|
private static boolean inLineOfSight(Level level, Vec3 spawnerPos, Vec3 mobPos) {
|
|
BlockHitResult blockHitResult = level.clip(new ClipContext(mobPos, spawnerPos, Block.VISUAL, Fluid.NONE, CollisionContext.empty()));
|
|
return blockHitResult.getBlockPos().equals(BlockPos.containing(spawnerPos)) || blockHitResult.getType() == Type.MISS;
|
|
}
|
|
|
|
public static void addSpawnParticles(Level level, BlockPos pos, RandomSource random, SimpleParticleType particleType) {
|
|
for (int i = 0; i < 20; i++) {
|
|
double d = pos.getX() + 0.5 + (random.nextDouble() - 0.5) * 2.0;
|
|
double e = pos.getY() + 0.5 + (random.nextDouble() - 0.5) * 2.0;
|
|
double f = pos.getZ() + 0.5 + (random.nextDouble() - 0.5) * 2.0;
|
|
level.addParticle(ParticleTypes.SMOKE, d, e, f, 0.0, 0.0, 0.0);
|
|
level.addParticle(particleType, d, e, f, 0.0, 0.0, 0.0);
|
|
}
|
|
}
|
|
|
|
public static void addBecomeOminousParticles(Level level, BlockPos pos, RandomSource random) {
|
|
for (int i = 0; i < 20; i++) {
|
|
double d = pos.getX() + 0.5 + (random.nextDouble() - 0.5) * 2.0;
|
|
double e = pos.getY() + 0.5 + (random.nextDouble() - 0.5) * 2.0;
|
|
double f = pos.getZ() + 0.5 + (random.nextDouble() - 0.5) * 2.0;
|
|
double g = random.nextGaussian() * 0.02;
|
|
double h = random.nextGaussian() * 0.02;
|
|
double j = random.nextGaussian() * 0.02;
|
|
level.addParticle(ParticleTypes.TRIAL_OMEN, d, e, f, g, h, j);
|
|
level.addParticle(ParticleTypes.SOUL_FIRE_FLAME, d, e, f, g, h, j);
|
|
}
|
|
}
|
|
|
|
public static void addDetectPlayerParticles(Level level, BlockPos pos, RandomSource random, int type, ParticleOptions particle) {
|
|
for (int i = 0; i < 30 + Math.min(type, 10) * 5; i++) {
|
|
double d = (2.0F * random.nextFloat() - 1.0F) * 0.65;
|
|
double e = (2.0F * random.nextFloat() - 1.0F) * 0.65;
|
|
double f = pos.getX() + 0.5 + d;
|
|
double g = pos.getY() + 0.1 + random.nextFloat() * 0.8;
|
|
double h = pos.getZ() + 0.5 + e;
|
|
level.addParticle(particle, f, g, h, 0.0, 0.0, 0.0);
|
|
}
|
|
}
|
|
|
|
public static void addEjectItemParticles(Level level, BlockPos pos, RandomSource random) {
|
|
for (int i = 0; i < 20; i++) {
|
|
double d = pos.getX() + 0.4 + random.nextDouble() * 0.2;
|
|
double e = pos.getY() + 0.4 + random.nextDouble() * 0.2;
|
|
double f = pos.getZ() + 0.4 + random.nextDouble() * 0.2;
|
|
double g = random.nextGaussian() * 0.02;
|
|
double h = random.nextGaussian() * 0.02;
|
|
double j = random.nextGaussian() * 0.02;
|
|
level.addParticle(ParticleTypes.SMALL_FLAME, d, e, f, g, h, j * 0.25);
|
|
level.addParticle(ParticleTypes.SMOKE, d, e, f, g, h, j);
|
|
}
|
|
}
|
|
|
|
public void overrideEntityToSpawn(EntityType<?> entityType, Level level) {
|
|
this.data.reset();
|
|
this.normalConfig = Holder.direct(this.normalConfig.value().withSpawning(entityType));
|
|
this.ominousConfig = Holder.direct(this.ominousConfig.value().withSpawning(entityType));
|
|
this.setState(level, TrialSpawnerState.INACTIVE);
|
|
}
|
|
|
|
@Deprecated(
|
|
forRemoval = true
|
|
)
|
|
@VisibleForTesting
|
|
public void setPlayerDetector(PlayerDetector playerDetector) {
|
|
this.playerDetector = playerDetector;
|
|
}
|
|
|
|
@Deprecated(
|
|
forRemoval = true
|
|
)
|
|
@VisibleForTesting
|
|
public void overridePeacefulAndMobSpawnRule() {
|
|
this.overridePeacefulAndMobSpawnRule = true;
|
|
}
|
|
|
|
public static enum FlameParticle {
|
|
NORMAL(ParticleTypes.FLAME),
|
|
OMINOUS(ParticleTypes.SOUL_FIRE_FLAME);
|
|
|
|
public final SimpleParticleType particleType;
|
|
|
|
private FlameParticle(final SimpleParticleType particleType) {
|
|
this.particleType = particleType;
|
|
}
|
|
|
|
public static TrialSpawner.FlameParticle decode(int id) {
|
|
TrialSpawner.FlameParticle[] flameParticles = values();
|
|
return id <= flameParticles.length && id >= 0 ? flameParticles[id] : NORMAL;
|
|
}
|
|
|
|
public int encode() {
|
|
return this.ordinal();
|
|
}
|
|
}
|
|
|
|
public interface StateAccessor {
|
|
void setState(Level level, TrialSpawnerState state);
|
|
|
|
TrialSpawnerState getState();
|
|
|
|
void markUpdated();
|
|
}
|
|
}
|