minecraft-src/net/minecraft/world/level/block/entity/trialspawner/TrialSpawner.java
2025-07-04 03:45:38 +03:00

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();
}
}