diff --git a/src/main/java/com/altnoir/poopsky/Config.java b/src/main/java/com/altnoir/poopsky/Config.java index abbdea33..1cabf9dd 100644 --- a/src/main/java/com/altnoir/poopsky/Config.java +++ b/src/main/java/com/altnoir/poopsky/Config.java @@ -19,7 +19,7 @@ public class Config { private static final ModConfigSpec.BooleanValue SET_POOPSKY_DEFAULT = BUILDER .comment("Whether the dedicated server level-type default should be set to poopsky") .translation("poopsky.configuration.setPoopskyDefault") - .define("setPoopskyDefault", false); + .define("setPoopskyDefault", true); private static final ModConfigSpec.BooleanValue VOID_NETHER_GENERATION = BUILDER .comment("Whether the custom void generator should also keep the nether empty") .translation("poopsky.configuration.voidNetherGeneration") diff --git a/src/main/java/com/altnoir/poopsky/PoopSky.java b/src/main/java/com/altnoir/poopsky/PoopSky.java index 787df63b..1574a008 100644 --- a/src/main/java/com/altnoir/poopsky/PoopSky.java +++ b/src/main/java/com/altnoir/poopsky/PoopSky.java @@ -9,6 +9,7 @@ import com.altnoir.poopsky.network.PSNetworking; import com.altnoir.poopsky.villager.PSVillagers; import com.altnoir.poopsky.worldgen.PSChunkGenerators; +import com.altnoir.poopsky.worldgen.PSStructures; import com.altnoir.poopsky.worldgen.foliage.PSFoliagePlacerTypes; import com.mojang.logging.LogUtils; import net.minecraft.core.BlockPos; @@ -52,6 +53,7 @@ public PoopSky(IEventBus modEventBus, ModContainer modContainer) { PSItems.register(modEventBus); PEntityType.register(modEventBus); PSFoliagePlacerTypes.register(modEventBus); + PSStructures.register(modEventBus); PSChunkGenerators.register(modEventBus); PSItemGroups.register(modEventBus); diff --git a/src/main/java/com/altnoir/poopsky/entity/p/PoolimeEntity.java b/src/main/java/com/altnoir/poopsky/entity/p/PoolimeEntity.java index 5fa367b1..85860a29 100644 --- a/src/main/java/com/altnoir/poopsky/entity/p/PoolimeEntity.java +++ b/src/main/java/com/altnoir/poopsky/entity/p/PoolimeEntity.java @@ -1,17 +1,28 @@ package com.altnoir.poopsky.entity.p; +import com.altnoir.poopsky.PoopSky; import com.altnoir.poopsky.block.PSBlocks; import com.altnoir.poopsky.init.PParticles; +import com.altnoir.poopsky.init.PSoundEvents; +import net.minecraft.core.registries.Registries; import net.minecraft.core.BlockPos; import net.minecraft.core.particles.ParticleOptions; +import net.minecraft.server.level.ServerLevel; +import net.minecraft.sounds.SoundEvent; import net.minecraft.util.RandomSource; +import net.minecraft.world.Difficulty; +import net.minecraft.world.damagesource.DamageSource; import net.minecraft.world.entity.EntityType; +import net.minecraft.world.entity.LivingEntity; +import net.minecraft.world.entity.Mob; import net.minecraft.world.entity.MobSpawnType; import net.minecraft.world.entity.ai.attributes.AttributeSupplier; import net.minecraft.world.entity.monster.Monster; import net.minecraft.world.entity.monster.Slime; +import net.minecraft.world.item.enchantment.EnchantmentHelper; import net.minecraft.world.level.Level; import net.minecraft.world.level.LevelAccessor; +import net.minecraft.world.level.levelgen.structure.Structure; import org.jetbrains.annotations.NotNull; public class PoolimeEntity extends Slime { @@ -28,10 +39,59 @@ public static AttributeSupplier.Builder createAttributes() { return PParticles.POOP_PARTICLE.get(); } - public static boolean checkPoolimeSpawnRules( - EntityType poolime, LevelAccessor level, MobSpawnType spawnType, BlockPos pos, RandomSource random - ) { - boolean flag = MobSpawnType.ignoresLightRequirements(spawnType); - return level.getBlockState(pos.below()).is(PSBlocks.POOLIME_POOP_BLOCK.get()) && flag; + @Override + protected SoundEvent getHurtSound(DamageSource damageSource) { + return this.isTiny() ? PSoundEvents.ENTITY_POOLIME_HURT_SMALL.get() : PSoundEvents.ENTITY_POOLIME_HURT.get(); + } + + @Override + protected SoundEvent getDeathSound() { + return this.isTiny() ? PSoundEvents.ENTITY_POOLIME_DEATH_SMALL.get() : PSoundEvents.ENTITY_POOLIME_DEATH.get(); + } + + @Override + protected SoundEvent getSquishSound() { + return this.isTiny() ? PSoundEvents.ENTITY_POOLIME_SQUISH_SMALL.get() : PSoundEvents.ENTITY_POOLIME_SQUISH.get(); + } + + @Override + protected SoundEvent getJumpSound() { + return this.isTiny() ? PSoundEvents.ENTITY_POOLIME_JUMP_SMALL.get() : PSoundEvents.ENTITY_POOLIME_JUMP.get(); + } + + @Override + protected void dealDamage(LivingEntity livingEntity) { + if (this.isAlive() && this.isWithinMeleeAttackRange(livingEntity) && this.hasLineOfSight(livingEntity)) { + DamageSource damageSource = this.damageSources().mobAttack(this); + if (livingEntity.hurt(damageSource, this.getAttackDamage())) { + this.playSound(PSoundEvents.ENTITY_POOLIME_ATTACK.get(), 1.0F, (this.random.nextFloat() - this.random.nextFloat()) * 0.2F + 1.0F); + if (this.level() instanceof ServerLevel serverLevel) { + EnchantmentHelper.doPostAttackEffects(serverLevel, livingEntity, damageSource); + } + } + } + } + + public static boolean checkPoolimeSpawnRules(EntityType poolime, LevelAccessor level, MobSpawnType spawnType, BlockPos pos, RandomSource random) { + if (level.getDifficulty() == Difficulty.PEACEFUL || !Mob.checkMobSpawnRules(poolime, level, spawnType, pos, random)) { + return false; + } + + if (MobSpawnType.ignoresLightRequirements(spawnType)) { + return true; + } + + return level.getBlockState(pos.below()).is(PSBlocks.POOLIME_POOP_BLOCK.get()) || isInPoopIsland(level, pos); + } + + private static boolean isInPoopIsland(LevelAccessor level, BlockPos pos) { + if (!(level instanceof ServerLevel serverLevel)) { + return false; + } + + Structure structure = serverLevel.registryAccess() + .registryOrThrow(Registries.STRUCTURE) + .get(PoopSky.loc("poop_island")); + return structure != null && serverLevel.structureManager().getStructureAt(pos, structure).isValid(); } -} \ No newline at end of file +} diff --git a/src/main/java/com/altnoir/poopsky/event/PSGameEvents.java b/src/main/java/com/altnoir/poopsky/event/PSGameEvents.java index a8bfbd6b..4977eae6 100644 --- a/src/main/java/com/altnoir/poopsky/event/PSGameEvents.java +++ b/src/main/java/com/altnoir/poopsky/event/PSGameEvents.java @@ -12,6 +12,7 @@ import com.altnoir.poopsky.villager.PSVillagerBehaviors; import com.altnoir.poopsky.villager.PSVillagerTrades; import com.altnoir.poopsky.worldgen.PSVoidChunkGenerator; +import com.altnoir.poopsky.worldgen.structure.PoopIslandStructure; import net.minecraft.core.BlockPos; import net.minecraft.server.level.ServerLevel; import net.minecraft.sounds.SoundEvent; @@ -26,6 +27,7 @@ import net.minecraft.world.item.*; import net.minecraft.world.item.alchemy.PotionBrewing; import net.minecraft.world.item.alchemy.Potions; +import net.minecraft.world.level.ChunkPos; import net.minecraft.world.level.ClipContext; import net.minecraft.world.level.GameRules; import net.minecraft.world.level.Level; @@ -147,8 +149,14 @@ public static void createSpawnToilet(LevelEvent.CreateSpawnPosition event) { level.setBlock(pos, AllToiletBlocks.OAK_TOILET.get().defaultBlockState(), 2); event.setCanceled(true); - event.getSettings().setSpawn(level.getHeightmapPos(Heightmap.Types.WORLD_SURFACE_WG, pos), 90.0F); + BlockPos spawn = level.getHeightmapPos(Heightmap.Types.WORLD_SURFACE_WG, pos); + event.getSettings().setSpawn(spawn, 90.0F); level.getGameRules().getRule(GameRules.RULE_SPAWN_RADIUS).set(0, level.getServer()); + + PoopIslandStructure.registerGuaranteedSpawn(level.getSeed(), spawn); + BlockPos islandCenter = PoopIslandStructure.getGuaranteedSpawnIslandCenter(level.getSeed(), spawn); + ChunkPos islandChunk = new ChunkPos(islandCenter); + level.getChunk(islandChunk.x, islandChunk.z); } } } diff --git a/src/main/java/com/altnoir/poopsky/init/PSoundEvents.java b/src/main/java/com/altnoir/poopsky/init/PSoundEvents.java index 2ee41c70..6f2dd6c0 100644 --- a/src/main/java/com/altnoir/poopsky/init/PSoundEvents.java +++ b/src/main/java/com/altnoir/poopsky/init/PSoundEvents.java @@ -19,6 +19,15 @@ public class PSoundEvents { public static final Supplier BLOCK_COMPOOPER_MAGGOTS = registerSoundEvent("block.compooper.maggots"); public static final Supplier ENTITY_VILLAGER_WORK_COMPOOPER = registerSoundEvent("entity.villager.work_compooper"); public static final Supplier ENTITY_VILLAGER_WORK_TOILET = registerSoundEvent("entity.villager.work_toilet"); + public static final Supplier ENTITY_POOLIME_ATTACK = registerSoundEvent("entity.poolime.attack"); + public static final Supplier ENTITY_POOLIME_DEATH = registerSoundEvent("entity.poolime.death"); + public static final Supplier ENTITY_POOLIME_DEATH_SMALL = registerSoundEvent("entity.poolime.death_small"); + public static final Supplier ENTITY_POOLIME_HURT = registerSoundEvent("entity.poolime.hurt"); + public static final Supplier ENTITY_POOLIME_HURT_SMALL = registerSoundEvent("entity.poolime.hurt_small"); + public static final Supplier ENTITY_POOLIME_JUMP = registerSoundEvent("entity.poolime.jump"); + public static final Supplier ENTITY_POOLIME_JUMP_SMALL = registerSoundEvent("entity.poolime.jump_small"); + public static final Supplier ENTITY_POOLIME_SQUISH = registerSoundEvent("entity.poolime.squish"); + public static final Supplier ENTITY_POOLIME_SQUISH_SMALL = registerSoundEvent("entity.poolime.squish_small"); public static final Supplier LAWRENCE = registerSoundEvent("lawrence"); public static final ResourceKey LAWRENCE_KEY = registerJukeboxSong("lawrence"); diff --git a/src/main/java/com/altnoir/poopsky/worldgen/PSStructures.java b/src/main/java/com/altnoir/poopsky/worldgen/PSStructures.java new file mode 100644 index 00000000..c4111dc1 --- /dev/null +++ b/src/main/java/com/altnoir/poopsky/worldgen/PSStructures.java @@ -0,0 +1,31 @@ +package com.altnoir.poopsky.worldgen; + +import com.altnoir.poopsky.PoopSky; +import com.altnoir.poopsky.worldgen.structure.PoopIslandPiece; +import com.altnoir.poopsky.worldgen.structure.PoopIslandStructure; +import net.minecraft.core.registries.Registries; +import net.minecraft.world.level.levelgen.structure.StructureType; +import net.minecraft.world.level.levelgen.structure.pieces.StructurePieceType; +import net.neoforged.bus.api.IEventBus; +import net.neoforged.neoforge.registries.DeferredHolder; +import net.neoforged.neoforge.registries.DeferredRegister; + +public final class PSStructures { + public static final DeferredRegister> STRUCTURE_TYPES = + DeferredRegister.create(Registries.STRUCTURE_TYPE, PoopSky.MOD_ID); + public static final DeferredRegister STRUCTURE_PIECES = + DeferredRegister.create(Registries.STRUCTURE_PIECE, PoopSky.MOD_ID); + + public static final DeferredHolder, StructureType> POOP_ISLAND = + STRUCTURE_TYPES.register("poop_island", () -> () -> PoopIslandStructure.CODEC); + public static final DeferredHolder POOP_ISLAND_PIECE = + STRUCTURE_PIECES.register("poop_island", () -> (StructurePieceType.StructureTemplateType) PoopIslandPiece::new); + + private PSStructures() { + } + + public static void register(IEventBus eventBus) { + STRUCTURE_TYPES.register(eventBus); + STRUCTURE_PIECES.register(eventBus); + } +} diff --git a/src/main/java/com/altnoir/poopsky/worldgen/PSVoidChunkGenerator.java b/src/main/java/com/altnoir/poopsky/worldgen/PSVoidChunkGenerator.java index 7d90dac1..d563bd8e 100644 --- a/src/main/java/com/altnoir/poopsky/worldgen/PSVoidChunkGenerator.java +++ b/src/main/java/com/altnoir/poopsky/worldgen/PSVoidChunkGenerator.java @@ -1,15 +1,20 @@ package com.altnoir.poopsky.worldgen; import com.altnoir.poopsky.Config; +import com.altnoir.poopsky.PoopSky; +import com.altnoir.poopsky.worldgen.structure.PoopIslandStructure; +import com.mojang.datafixers.util.Pair; import com.mojang.datafixers.util.Either; import com.mojang.serialization.Codec; import com.mojang.serialization.MapCodec; import com.mojang.serialization.codecs.RecordCodecBuilder; +import net.minecraft.server.level.ServerLevel; import net.minecraft.CrashReport; import net.minecraft.ReportedException; import net.minecraft.SharedConstants; import net.minecraft.core.BlockPos; import net.minecraft.core.Holder; +import net.minecraft.core.HolderSet; import net.minecraft.core.Registry; import net.minecraft.core.RegistryAccess; import net.minecraft.core.SectionPos; @@ -23,6 +28,7 @@ import net.minecraft.world.level.NoiseColumn; import net.minecraft.world.level.StructureManager; import net.minecraft.world.level.WorldGenLevel; +import net.minecraft.world.level.biome.Biome; import net.minecraft.world.level.biome.BiomeManager; import net.minecraft.world.level.biome.BiomeSource; import net.minecraft.world.level.block.Blocks; @@ -32,6 +38,7 @@ import net.minecraft.world.level.chunk.ChunkGeneratorStructureState; import net.minecraft.world.level.levelgen.GenerationStep; import net.minecraft.world.level.levelgen.Heightmap; +import net.minecraft.world.level.levelgen.LegacyRandomSource; import net.minecraft.world.level.levelgen.NoiseBasedChunkGenerator; import net.minecraft.world.level.levelgen.NoiseGeneratorSettings; import net.minecraft.world.level.levelgen.RandomState; @@ -42,18 +49,22 @@ import net.minecraft.world.level.levelgen.structure.BoundingBox; import net.minecraft.world.level.levelgen.structure.Structure; import net.minecraft.world.level.levelgen.structure.StructureSet; +import net.minecraft.world.level.levelgen.structure.StructureSet.StructureSelectionEntry; import net.minecraft.world.level.levelgen.structure.StructureStart; +import net.minecraft.world.level.levelgen.structure.placement.StructurePlacement; import net.minecraft.world.level.levelgen.structure.templatesystem.StructureTemplateManager; +import java.util.ArrayList; import java.util.Collections; -import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.Optional; import java.util.Set; import java.util.concurrent.CompletableFuture; +import java.util.function.Predicate; import java.util.function.Supplier; import java.util.stream.Collectors; +import javax.annotation.Nullable; public class PSVoidChunkGenerator extends NoiseBasedChunkGenerator { private static final int VIRTUAL_SURFACE_Y = 64; @@ -126,7 +137,19 @@ private static AllowedStructureSets ofKeys(List> keys) private boolean contains(Holder holder) { return tag.map(holder::is) - .orElseGet(() -> holder.unwrapKey().filter(keys::contains).isPresent()); + .orElseGet(() -> { + Set allowedLocations = keys.stream() + .map(ResourceKey::location) + .collect(Collectors.toUnmodifiableSet()); + + return holder.unwrapKey() + .map(key -> allowedLocations.contains(key.location())) + .orElse(false) + || holder.value().structures().stream() + .map(StructureSelectionEntry::structure) + .map(Holder::unwrapKey) + .anyMatch(key -> key.map(ResourceKey::location).filter(allowedLocations::contains).isPresent()); + }); } } @@ -196,22 +219,35 @@ public void createStructures( ChunkAccess chunk, StructureTemplateManager templateManager ) { - super.createStructures(registries, structureState, structureManager, chunk, templateManager); + if (generateNormal || allowedStructureSets.isEmpty()) { + super.createStructures(registries, structureState, structureManager, chunk, templateManager); + } else { + createAllowedStructures(registries, structureState, structureManager, chunk, templateManager); + } - if (allowedStructureSets.isEmpty()) { - return; + if (!generateNormal && settings.is(ResourceLocation.parse("minecraft:overworld")) && isStructureAllowed(registries, PoopSky.loc("poop_island"))) { + PoopIslandStructure.addGuaranteedSpawnStart(registries, chunk, structureManager, templateManager, structureState.getLevelSeed()); } + } - Set allowedStructures = resolveAllowedStructures(registries); - Map filteredStarts = new HashMap<>(); + @Override + @Nullable + public Pair> findNearestMapStructure(ServerLevel level, HolderSet structures, BlockPos pos, int searchRadius, boolean skipKnownStructures) { + Pair> guaranteedSpawnIsland = findGuaranteedSpawnIsland(level, structures, pos, searchRadius, skipKnownStructures); + if (generateNormal || allowedStructureSets.isEmpty()) { + return nearestStructure(pos, super.findNearestMapStructure(level, structures, pos, searchRadius, skipKnownStructures), guaranteedSpawnIsland); + } - chunk.getAllStarts().forEach((structure, start) -> { - if (allowedStructures.contains(structure)) { - filteredStarts.put(structure, start); - } - }); + Set allowedStructures = resolveAllowedStructures(level.registryAccess()); + List> searchableStructures = structures.stream() + .filter(structure -> allowedStructures.contains(structure.value())) + .toList(); + + if (searchableStructures.isEmpty()) { + return guaranteedSpawnIsland; + } - chunk.setAllStarts(filteredStarts); + return nearestStructure(pos, super.findNearestMapStructure(level, HolderSet.direct(searchableStructures), pos, searchRadius, skipKnownStructures), guaranteedSpawnIsland); } @Override @@ -249,11 +285,143 @@ private Set resolveAllowedStructures(RegistryAccess registries) { return registry.holders() .filter(allowed::contains) .flatMap(holder -> holder.value().structures().stream()) - .map(StructureSet.StructureSelectionEntry::structure) + .map(StructureSelectionEntry::structure) .map(Holder::value) .collect(Collectors.toUnmodifiableSet()); } + private boolean isStructureAllowed(RegistryAccess registries, ResourceLocation structureId) { + if (allowedStructureSets.isEmpty()) { + return true; + } + + Registry structureRegistry = registries.registryOrThrow(Registries.STRUCTURE); + Structure structure = structureRegistry.get(structureId); + return structure != null && resolveAllowedStructures(registries).contains(structure); + } + + @Nullable + private Pair> findGuaranteedSpawnIsland(ServerLevel level, HolderSet structures, BlockPos pos, int searchRadius, boolean skipKnownStructures) { + if (skipKnownStructures || generateNormal || !settings.is(ResourceLocation.parse("minecraft:overworld")) || !isStructureAllowed(level.registryAccess(), PoopSky.loc("poop_island"))) { + return null; + } + + Holder poopIsland = structures.stream() + .filter(structure -> structure.unwrapKey() + .map(key -> key.location().equals(PoopSky.loc("poop_island"))) + .orElse(false)) + .findFirst() + .orElse(null); + if (poopIsland == null) { + return null; + } + + BlockPos center = PoopIslandStructure.getGuaranteedSpawnIslandCenter(level); + int chunkDistance = Math.max( + Math.abs(SectionPos.blockToSectionCoord(pos.getX()) - SectionPos.blockToSectionCoord(center.getX())), + Math.abs(SectionPos.blockToSectionCoord(pos.getZ()) - SectionPos.blockToSectionCoord(center.getZ())) + ); + return chunkDistance <= searchRadius ? Pair.of(center, poopIsland) : null; + } + + @Nullable + private static Pair> nearestStructure(BlockPos pos, @Nullable Pair> first, @Nullable Pair> second) { + if (first == null) { + return second; + } + if (second == null) { + return first; + } + + return pos.distSqr(second.getFirst()) < pos.distSqr(first.getFirst()) ? second : first; + } + + private void createAllowedStructures(RegistryAccess registries, ChunkGeneratorStructureState structureState, StructureManager structureManager, ChunkAccess chunk, StructureTemplateManager templateManager) { + ChunkPos chunkPos = chunk.getPos(); + SectionPos sectionPos = SectionPos.bottomOf(chunk); + RandomState randomState = structureState.randomState(); + AllowedStructureSets allowed = allowedStructureSets.orElseThrow(); + + for (Holder structureSetHolder : structureState.possibleStructureSets()) { + if (!allowed.contains(structureSetHolder)) { + continue; + } + + StructureSet structureSet = structureSetHolder.value(); + StructurePlacement placement = structureSet.placement(); + List entries = structureSet.structures(); + + boolean hasExistingStart = false; + for (StructureSelectionEntry entry : entries) { + StructureStart start = structureManager.getStartForStructure(sectionPos, entry.structure().value(), chunk); + if (start != null && start.isValid()) { + hasExistingStart = true; + break; + } + } + + if (hasExistingStart || !placement.isStructureChunk(structureState, chunkPos.x, chunkPos.z)) { + continue; + } + + if (entries.size() == 1) { + tryGenerateStructure(entries.getFirst(), structureManager, registries, randomState, templateManager, structureState.getLevelSeed(), chunk, chunkPos, sectionPos); + continue; + } + + ArrayList shuffledEntries = new ArrayList<>(entries); + WorldgenRandom random = new WorldgenRandom(new LegacyRandomSource(0L)); + random.setLargeFeatureSeed(structureState.getLevelSeed(), chunkPos.x, chunkPos.z); + int totalWeight = 0; + + for (StructureSelectionEntry entry : shuffledEntries) { + totalWeight += entry.weight(); + } + + while (!shuffledEntries.isEmpty()) { + int targetWeight = random.nextInt(totalWeight); + int index = 0; + + for (StructureSelectionEntry entry : shuffledEntries) { + targetWeight -= entry.weight(); + if (targetWeight < 0) { + break; + } + + index++; + } + + StructureSelectionEntry entry = shuffledEntries.get(index); + if (tryGenerateStructure(entry, structureManager, registries, randomState, templateManager, structureState.getLevelSeed(), chunk, chunkPos, sectionPos)) { + break; + } + + shuffledEntries.remove(index); + totalWeight -= entry.weight(); + } + } + } + + private boolean tryGenerateStructure(StructureSelectionEntry structureSelectionEntry, StructureManager structureManager, + RegistryAccess registries, RandomState randomState, + StructureTemplateManager templateManager, long seed, + ChunkAccess chunk, ChunkPos chunkPos, SectionPos sectionPos + ) { + Structure structure = structureSelectionEntry.structure().value(); + StructureStart existingStart = structureManager.getStartForStructure(sectionPos, structure, chunk); + int references = existingStart != null ? existingStart.getReferences() : 0; + HolderSet biomes = structure.biomes(); + Predicate> biomePredicate = biomes::contains; + StructureStart start = structure.generate(registries, this, this.biomeSource, randomState, templateManager, seed, chunkPos, references, chunk, biomePredicate); + + if (start.isValid()) { + structureManager.setStartForStructure(sectionPos, structure, start, chunk); + return true; + } + + return false; + } + private void placeStructuresOnly( WorldGenLevel level, ChunkAccess chunk, StructureManager structureManager) { ChunkPos chunkPos = chunk.getPos(); @@ -265,14 +433,11 @@ private void placeStructuresOnly( WorldGenLevel level, ChunkAccess chunk, Struct BlockPos origin = sectionPos.origin(); RegistryAccess registries = level.registryAccess(); Registry structureRegistry = registries.registryOrThrow(Registries.STRUCTURE); - - Set allowedStructures = allowedStructureSets.isPresent() - ? resolveAllowedStructures(registries) - : null; + Set allowedStructures = resolveAllowedStructures(registries); Map> structuresByStep = structureRegistry.stream() - .filter(structure -> allowedStructures == null || allowedStructures.contains(structure)) + .filter(structure -> allowedStructureSets.isEmpty() || allowedStructures.contains(structure)) .collect(Collectors.groupingBy(structure -> structure.step().ordinal())); WorldgenRandom random = new WorldgenRandom(new XoroshiroRandomSource(RandomSupport.generateUniqueSeed())); @@ -349,4 +514,4 @@ private static BoundingBox writableArea(ChunkAccess chunk) { private static int virtualSurfaceY(LevelHeightAccessor level) { return Math.clamp(VIRTUAL_SURFACE_Y, level.getMinBuildHeight() + 1, level.getMaxBuildHeight()); } -} \ No newline at end of file +} diff --git a/src/main/java/com/altnoir/poopsky/worldgen/structure/PoopIslandPiece.java b/src/main/java/com/altnoir/poopsky/worldgen/structure/PoopIslandPiece.java new file mode 100644 index 00000000..774df4e8 --- /dev/null +++ b/src/main/java/com/altnoir/poopsky/worldgen/structure/PoopIslandPiece.java @@ -0,0 +1,164 @@ +package com.altnoir.poopsky.worldgen.structure; + +import com.altnoir.poopsky.block.PSBlocks; +import com.altnoir.poopsky.entity.p.PoolimeEntity; +import com.altnoir.poopsky.init.PEntityType; +import com.altnoir.poopsky.worldgen.PSStructures; +import net.minecraft.core.BlockPos; +import net.minecraft.nbt.CompoundTag; +import net.minecraft.resources.ResourceLocation; +import net.minecraft.util.Mth; +import net.minecraft.util.RandomSource; +import net.minecraft.world.level.ChunkPos; +import net.minecraft.world.level.ServerLevelAccessor; +import net.minecraft.world.level.StructureManager; +import net.minecraft.world.level.WorldGenLevel; +import net.minecraft.world.level.block.Blocks; +import net.minecraft.world.level.block.Rotation; +import net.minecraft.world.level.block.state.BlockState; +import net.minecraft.world.level.chunk.ChunkGenerator; +import net.minecraft.world.level.levelgen.structure.BoundingBox; +import net.minecraft.world.level.levelgen.structure.TemplateStructurePiece; +import net.minecraft.world.level.levelgen.structure.pieces.StructurePieceSerializationContext; +import net.minecraft.world.level.levelgen.structure.templatesystem.BlockIgnoreProcessor; +import net.minecraft.world.level.levelgen.structure.templatesystem.StructurePlaceSettings; +import net.minecraft.world.level.levelgen.structure.templatesystem.StructureTemplate; +import net.minecraft.world.level.levelgen.structure.templatesystem.StructureTemplateManager; + +import java.util.ArrayList; +import java.util.List; + +public class PoopIslandPiece extends TemplateStructurePiece { + private static final String ROTATION_KEY = "Rotation"; + + public PoopIslandPiece(StructureTemplateManager manager, ResourceLocation templateId, BlockPos pos, Rotation rotation) { + super(PSStructures.POOP_ISLAND_PIECE.get(), 0, manager, templateId, templateId.toString(), placeSettings(rotation), pos); + } + + public PoopIslandPiece(StructureTemplateManager manager, CompoundTag tag) { + super(PSStructures.POOP_ISLAND_PIECE.get(), tag, manager, id -> placeSettings(readRotation(tag))); + } + + public static StructurePlaceSettings placeSettings(Rotation rotation) { + return new StructurePlaceSettings() + .setRotation(rotation) + .setIgnoreEntities(false) + .setFinalizeEntities(true) + .addProcessor(BlockIgnoreProcessor.STRUCTURE_BLOCK); + } + + @Override + protected void addAdditionalSaveData(StructurePieceSerializationContext context, CompoundTag tag) { + super.addAdditionalSaveData(context, tag); + tag.putString(ROTATION_KEY, this.placeSettings.getRotation().name()); + } + + @Override + public void postProcess( + WorldGenLevel level, + StructureManager structureManager, + ChunkGenerator generator, + RandomSource random, + BoundingBox box, + ChunkPos chunkPos, + BlockPos pos + ) { + super.postProcess(level, structureManager, generator, random, box, chunkPos, pos); + RandomSource islandRandom = RandomSource.create(Mth.getSeed(this.templatePosition)); + placeRandomPoopTree(level, islandRandom, this.template, this.templatePosition, this.placeSettings, box); + spawnRandomPoolimes(level, islandRandom, this.template, this.templatePosition, this.placeSettings, box); + } + + @Override + protected void handleDataMarker(String name, BlockPos pos, ServerLevelAccessor level, RandomSource random, BoundingBox box) { + } + + public static void placeRandomPoopTree(WorldGenLevel level, RandomSource random, StructureTemplate template, BlockPos origin, StructurePlaceSettings settings, BoundingBox box) { + List treeMarkers = template.filterBlocks(origin, settings, Blocks.STRUCTURE_BLOCK) + .stream() + .filter(PoopIslandPiece::isTreeMarker) + .toList(); + + if (treeMarkers.isEmpty() || random.nextFloat() >= 0.7F) { + return; + } + + BlockPos treePos = treeMarkers.get(random.nextInt(treeMarkers.size())).pos(); + placePoopTree(level, random, treePos); + } + + public static void spawnRandomPoolimes(WorldGenLevel level, RandomSource random, StructureTemplate template, BlockPos origin, StructurePlaceSettings settings, BoundingBox box) { + List poolimeBlocks = new ArrayList<>(template.filterBlocks(origin, settings, PSBlocks.POOLIME_BLOCK.get()) + .stream() + .filter(blockInfo -> box.isInside(blockInfo.pos().above())) + .filter(blockInfo -> level.getBlockState(blockInfo.pos().above()).canBeReplaced()) + .toList()); + + if (poolimeBlocks.isEmpty()) { + return; + } + + int spawnCount = Math.min(poolimeBlocks.size(), random.nextIntBetweenInclusive(1, 3)); + for (int index = 0; index < spawnCount; index++) { + BlockPos pos = poolimeBlocks.remove(random.nextInt(poolimeBlocks.size())).pos().above(); + PoolimeEntity poolime = PEntityType.POOLIME.get().create(level.getLevel()); + if (poolime == null) { + continue; + } + + poolime.setSize(random.nextInt(3) + 1, true); + poolime.moveTo(pos.getX() + 0.5D, pos.getY(), pos.getZ() + 0.5D, random.nextFloat() * 360.0F, 0.0F); + if (level.noCollision(poolime, poolime.getBoundingBox())) { + level.addFreshEntity(poolime); + } + } + } + + private static void placePoopTree(WorldGenLevel level, RandomSource random, BlockPos basePos) { + int trunkHeight = random.nextIntBetweenInclusive(4, 5); + for (int y = 0; y < trunkHeight; y++) { + placeTreeBlock(level, basePos.above(y), PSBlocks.POOP_LOG.get().defaultBlockState()); + } + + BlockPos crown = basePos.above(trunkHeight); + for (int y = -2; y <= 1; y++) { + int radius = y == 1 ? 1 : 2; + for (int x = -radius; x <= radius; x++) { + for (int z = -radius; z <= radius; z++) { + if (Math.abs(x) == radius && Math.abs(z) == radius && random.nextBoolean()) { + continue; + } + + placeTreeBlock(level, crown.offset(x, y, z), PSBlocks.POOP_LEAVES.get().defaultBlockState()); + } + } + } + } + + private static void placeTreeBlock(WorldGenLevel level, BlockPos pos, BlockState state) { + if (level.isOutsideBuildHeight(pos)) { + return; + } + + if (level.getBlockState(pos).canBeReplaced()) { + level.setBlock(pos, state, 2); + } + } + + private static boolean isTreeMarker(StructureTemplate.StructureBlockInfo blockInfo) { + CompoundTag tag = blockInfo.nbt(); + return tag != null && PoopIslandStructure.POOP_TREE_MARKER.equals(tag.getString("metadata")); + } + + private static Rotation readRotation(CompoundTag tag) { + if (!tag.contains(ROTATION_KEY)) { + return Rotation.NONE; + } + + try { + return Rotation.valueOf(tag.getString(ROTATION_KEY)); + } catch (IllegalArgumentException exception) { + return Rotation.NONE; + } + } +} diff --git a/src/main/java/com/altnoir/poopsky/worldgen/structure/PoopIslandStructure.java b/src/main/java/com/altnoir/poopsky/worldgen/structure/PoopIslandStructure.java new file mode 100644 index 00000000..016d0b90 --- /dev/null +++ b/src/main/java/com/altnoir/poopsky/worldgen/structure/PoopIslandStructure.java @@ -0,0 +1,168 @@ +package com.altnoir.poopsky.worldgen.structure; + +import com.altnoir.poopsky.PoopSky; +import com.altnoir.poopsky.worldgen.PSStructures; +import com.mojang.serialization.MapCodec; +import net.minecraft.core.BlockPos; +import net.minecraft.core.Registry; +import net.minecraft.core.RegistryAccess; +import net.minecraft.core.SectionPos; +import net.minecraft.core.Vec3i; +import net.minecraft.core.registries.Registries; +import net.minecraft.resources.ResourceLocation; +import net.minecraft.server.level.ServerLevel; +import net.minecraft.util.Mth; +import net.minecraft.util.RandomSource; +import net.minecraft.world.level.ChunkPos; +import net.minecraft.world.level.LevelHeightAccessor; +import net.minecraft.world.level.StructureManager; +import net.minecraft.world.level.block.Rotation; +import net.minecraft.world.level.chunk.ChunkAccess; +import net.minecraft.world.level.levelgen.Heightmap; +import net.minecraft.world.level.levelgen.XoroshiroRandomSource; +import net.minecraft.world.level.levelgen.structure.Structure; +import net.minecraft.world.level.levelgen.structure.StructureStart; +import net.minecraft.world.level.levelgen.structure.StructureType; +import net.minecraft.world.level.levelgen.structure.pieces.StructurePiecesBuilder; +import net.minecraft.world.level.levelgen.structure.templatesystem.StructureTemplate; +import net.minecraft.world.level.levelgen.structure.templatesystem.StructureTemplateManager; + +import java.util.List; +import java.util.Map; +import java.util.Optional; +import java.util.concurrent.ConcurrentHashMap; + +public class PoopIslandStructure extends Structure { + public static final MapCodec CODEC = simpleCodec(PoopIslandStructure::new); + public static final List DIRT_ISLAND_TEMPLATES = List.of( + PoopSky.loc("islands/dirt/0x1x0"), + PoopSky.loc("islands/dirt/11x1x11"), + PoopSky.loc("islands/dirt/2x2x4"), + PoopSky.loc("islands/dirt/4x1x6"), + PoopSky.loc("islands/dirt/8x1x11") + ); + public static final String POOP_TREE_MARKER = "poopsky:poop_tree"; + private static final int OVERWORLD_HEIGHT_OFFSET = 64; + private static final int VOID_ISLAND_Y = 128; + private static final int SPAWN_ISLAND_MIN_DISTANCE = 100; + private static final int SPAWN_ISLAND_MAX_DISTANCE = 200; + private static final Map GUARANTEED_SPAWNS = new ConcurrentHashMap<>(); + + public PoopIslandStructure(StructureSettings settings) { + super(settings); + } + + @Override + protected Optional findGenerationPoint(GenerationContext context) { + ResourceLocation templateId = randomTemplate(context.random()); + Optional template = context.structureTemplateManager().get(templateId); + if (template.isEmpty()) { + PoopSky.LOGGER.warn("Missing poop island template {}", templateId); + return Optional.empty(); + } + + Rotation rotation = Rotation.getRandom(context.random()); + ChunkPos chunkPos = context.chunkPos(); + int centerX = chunkPos.getMiddleBlockX(); + int centerZ = chunkPos.getMiddleBlockZ(); + int y = clampIslandY(context.heightAccessor(), + context.chunkGenerator().getFirstOccupiedHeight(centerX, centerZ, Heightmap.Types.WORLD_SURFACE_WG, context.heightAccessor(), context.randomState()) + + OVERWORLD_HEIGHT_OFFSET); + Vec3i size = template.get().getSize(rotation); + BlockPos origin = new BlockPos(centerX - size.getX() / 2, y, centerZ - size.getZ() / 2); + + return Optional.of(new GenerationStub(origin, builder -> + builder.addPiece(new PoopIslandPiece(context.structureTemplateManager(), templateId, origin, rotation)))); + } + + @Override + public StructureType type() { + return PSStructures.POOP_ISLAND.get(); + } + + public static void addGuaranteedSpawnStart( + RegistryAccess registries, + ChunkAccess chunk, + StructureManager structureManager, + StructureTemplateManager templateManager, + long seed + ) { + BlockPos registeredSpawn = GUARANTEED_SPAWNS.get(seed); + BlockPos center = registeredSpawn != null ? getGuaranteedSpawnIslandCenter(seed, registeredSpawn) : getGuaranteedSpawnIslandCenter(seed); + if (!chunk.getPos().equals(new ChunkPos(center))) { + return; + } + + Registry structureRegistry = registries.registryOrThrow(Registries.STRUCTURE); + Structure structure = structureRegistry.get(PoopSky.loc("poop_island")); + if (!(structure instanceof PoopIslandStructure)) { + PoopSky.LOGGER.warn("Missing poop island structure"); + return; + } + + SectionPos sectionPos = SectionPos.bottomOf(chunk); + StructureStart existingStart = structureManager.getStartForStructure(sectionPos, structure, chunk); + if (existingStart != null && existingStart.isValid()) { + return; + } + + int islandY = clampIslandY(chunk.getHeightAccessorForGeneration(), VOID_ISLAND_Y); + RandomSource random = RandomSource.create(seed ^ Mth.getSeed(center)); + ResourceLocation templateId = randomTemplate(random); + StructureTemplate template = templateManager.get(templateId).orElse(null); + if (template == null) { + PoopSky.LOGGER.warn("Missing poop island template {}", templateId); + return; + } + + Rotation rotation = Rotation.getRandom(random); + Vec3i size = template.getSize(rotation); + BlockPos origin = new BlockPos(center.getX() - size.getX() / 2, islandY, center.getZ() - size.getZ() / 2); + StructurePiecesBuilder builder = new StructurePiecesBuilder(); + builder.addPiece(new PoopIslandPiece(templateManager, templateId, origin, rotation)); + structureManager.setStartForStructure(sectionPos, structure, new StructureStart(structure, chunk.getPos(), 0, builder.build()), chunk); + } + + public static void registerGuaranteedSpawn(long seed, BlockPos spawn) { + GUARANTEED_SPAWNS.put(seed, spawn.immutable()); + } + + public static BlockPos getGuaranteedSpawnIslandCenter(long seed, BlockPos spawn) { + RandomSource random = RandomSource.create(seed ^ Mth.getSeed(spawn)); + int distance = random.nextIntBetweenInclusive(SPAWN_ISLAND_MIN_DISTANCE, SPAWN_ISLAND_MAX_DISTANCE); + double angle = random.nextDouble() * Math.TAU; + + return new BlockPos( + spawn.getX() + (int) Math.round(Math.cos(angle) * distance), + 0, + spawn.getZ() + (int) Math.round(Math.sin(angle) * distance) + ); + } + + private static BlockPos getGuaranteedSpawnIslandCenter(long seed) { + RandomSource spawnRandom = new XoroshiroRandomSource(seed); + return getGuaranteedSpawnIslandCenter(seed, new BlockPos( + spawnRandom.nextIntBetweenInclusive(-200, 200), + 87, + spawnRandom.nextIntBetweenInclusive(-200, 200) + )); + } + + public static BlockPos getGuaranteedSpawnIslandCenter(ServerLevel level) { + BlockPos registeredSpawn = GUARANTEED_SPAWNS.get(level.getSeed()); + + if (registeredSpawn != null) { + return getGuaranteedSpawnIslandCenter(level.getSeed(), registeredSpawn); + } + + return getGuaranteedSpawnIslandCenter(level.getSeed(), level.getSharedSpawnPos()); + } + + private static ResourceLocation randomTemplate(RandomSource random) { + return DIRT_ISLAND_TEMPLATES.get(random.nextInt(DIRT_ISLAND_TEMPLATES.size())); + } + + private static int clampIslandY(LevelHeightAccessor level, int y) { + return Math.clamp(y, level.getMinBuildHeight() + 8, level.getMaxBuildHeight() - 32); + } +} diff --git a/src/main/resources/assets/poopsky/lang/en_us.json b/src/main/resources/assets/poopsky/lang/en_us.json index 63264832..c7570205 100644 --- a/src/main/resources/assets/poopsky/lang/en_us.json +++ b/src/main/resources/assets/poopsky/lang/en_us.json @@ -263,6 +263,10 @@ "subtitle.poopsky.compooper.maggots": "Compooper maggots", "subtitle.poopsky.villager.work_compooper": "Poopmaker works", "subtitle.poopsky.villager.work_toilet": "Gastronome eats", + "subtitle.poopsky.poolime.attack": "Poolime attacks", + "subtitle.poopsky.poolime.death": "Poolime dies", + "subtitle.poopsky.poolime.hurt": "Poolime hurts", + "subtitle.poopsky.poolime.squish": "Poolime squishes", "tag.item.poopsky.compooper_saplings": "Compooper Saplings", "jei.category.poopsky.compooper": "Compooper", diff --git a/src/main/resources/assets/poopsky/lang/zh_cn.json b/src/main/resources/assets/poopsky/lang/zh_cn.json index 68e80ee3..cdbed34e 100644 --- a/src/main/resources/assets/poopsky/lang/zh_cn.json +++ b/src/main/resources/assets/poopsky/lang/zh_cn.json @@ -263,6 +263,10 @@ "subtitle.poopsky.compooper.maggots": "堆粪桶:产蛆", "subtitle.poopsky.villager.work_compooper": "养粪人:工作", "subtitle.poopsky.villager.work_toilet": "美食家:进食", + "subtitle.poopsky.poolime.attack": "屎莱姆:攻击", + "subtitle.poopsky.poolime.death": "屎莱姆:死亡", + "subtitle.poopsky.poolime.hurt": "屎莱姆:受伤", + "subtitle.poopsky.poolime.squish": "屎莱姆:挤压", "tag.item.poopsky.compooper_saplings": "堆粪桶可产出树苗", "jei.category.poopsky.compooper": "堆粪桶", diff --git a/src/main/resources/assets/poopsky/sounds.json b/src/main/resources/assets/poopsky/sounds.json index bad9a642..d15ce32e 100644 --- a/src/main/resources/assets/poopsky/sounds.json +++ b/src/main/resources/assets/poopsky/sounds.json @@ -44,6 +44,87 @@ "random/burp" ] }, + "entity.poolime.attack": { + "subtitle": "subtitle.poopsky.poolime.attack", + "sounds": [ + { + "name": "minecraft:entity.slime.attack", + "type": "event" + } + ] + }, + "entity.poolime.death": { + "subtitle": "subtitle.poopsky.poolime.death", + "sounds": [ + { + "name": "minecraft:entity.slime.death", + "type": "event" + } + ] + }, + "entity.poolime.death_small": { + "subtitle": "subtitle.poopsky.poolime.death", + "sounds": [ + { + "name": "minecraft:entity.slime.death_small", + "type": "event" + } + ] + }, + "entity.poolime.hurt": { + "subtitle": "subtitle.poopsky.poolime.hurt", + "sounds": [ + { + "name": "minecraft:entity.slime.hurt", + "type": "event" + } + ] + }, + "entity.poolime.hurt_small": { + "subtitle": "subtitle.poopsky.poolime.hurt", + "sounds": [ + { + "name": "minecraft:entity.slime.hurt_small", + "type": "event" + } + ] + }, + "entity.poolime.jump": { + "subtitle": "subtitle.poopsky.poolime.squish", + "sounds": [ + { + "name": "minecraft:entity.slime.jump", + "type": "event" + } + ] + }, + "entity.poolime.jump_small": { + "subtitle": "subtitle.poopsky.poolime.squish", + "sounds": [ + { + "name": "minecraft:entity.slime.jump_small", + "type": "event" + } + ] + }, + "entity.poolime.squish": { + "subtitle": "subtitle.poopsky.poolime.squish", + "sounds": [ + { + "name": "minecraft:entity.slime.squish", + "type": "event" + } + ] + }, + "entity.poolime.squish_small": { + "subtitle": "subtitle.poopsky.poolime.squish", + "sounds": [ + { + "name": "minecraft:entity.slime.squish_small", + "type": "event" + } + ] + }, "lawrence": { "sounds": [ { @@ -68,4 +149,4 @@ } ] } -} \ No newline at end of file +} diff --git a/src/main/resources/data/poopsky/structure/islands/dirt/0x1x0.nbt b/src/main/resources/data/poopsky/structure/islands/dirt/0x1x0.nbt new file mode 100644 index 00000000..e1e6ba55 Binary files /dev/null and b/src/main/resources/data/poopsky/structure/islands/dirt/0x1x0.nbt differ diff --git a/src/main/resources/data/poopsky/structure/islands/dirt/11x1x11.nbt b/src/main/resources/data/poopsky/structure/islands/dirt/11x1x11.nbt new file mode 100644 index 00000000..56092d68 Binary files /dev/null and b/src/main/resources/data/poopsky/structure/islands/dirt/11x1x11.nbt differ diff --git a/src/main/resources/data/poopsky/structure/islands/dirt/2x2x4.nbt b/src/main/resources/data/poopsky/structure/islands/dirt/2x2x4.nbt new file mode 100644 index 00000000..37a80e02 Binary files /dev/null and b/src/main/resources/data/poopsky/structure/islands/dirt/2x2x4.nbt differ diff --git a/src/main/resources/data/poopsky/structure/islands/dirt/4x1x6.nbt b/src/main/resources/data/poopsky/structure/islands/dirt/4x1x6.nbt new file mode 100644 index 00000000..755fc8c1 Binary files /dev/null and b/src/main/resources/data/poopsky/structure/islands/dirt/4x1x6.nbt differ diff --git a/src/main/resources/data/poopsky/structure/islands/dirt/8x1x11.nbt b/src/main/resources/data/poopsky/structure/islands/dirt/8x1x11.nbt new file mode 100644 index 00000000..9729a7b7 Binary files /dev/null and b/src/main/resources/data/poopsky/structure/islands/dirt/8x1x11.nbt differ diff --git a/src/main/resources/data/poopsky/worldgen/structure/poop_island.json b/src/main/resources/data/poopsky/worldgen/structure/poop_island.json new file mode 100644 index 00000000..d53743d1 --- /dev/null +++ b/src/main/resources/data/poopsky/worldgen/structure/poop_island.json @@ -0,0 +1,19 @@ +{ + "type": "poopsky:poop_island", + "biomes": "#minecraft:is_overworld", + "spawn_overrides": { + "monster": { + "bounding_box": "full", + "spawns": [ + { + "type": "poopsky:poolime", + "weight": 100, + "minCount": 1, + "maxCount": 3 + } + ] + } + }, + "step": "surface_structures", + "terrain_adaptation": "none" +} diff --git a/src/main/resources/data/poopsky/worldgen/structure_set/poop_islands.json b/src/main/resources/data/poopsky/worldgen/structure_set/poop_islands.json new file mode 100644 index 00000000..50b6afe4 --- /dev/null +++ b/src/main/resources/data/poopsky/worldgen/structure_set/poop_islands.json @@ -0,0 +1,14 @@ +{ + "placement": { + "type": "minecraft:random_spread", + "salt": 70071103, + "separation": 12, + "spacing": 40 + }, + "structures": [ + { + "structure": "poopsky:poop_island", + "weight": 1 + } + ] +} diff --git a/src/main/resources/data/poopsky/worldgen/world_preset/poopsky.json b/src/main/resources/data/poopsky/worldgen/world_preset/poopsky.json index 60c53725..dc3bf09e 100644 --- a/src/main/resources/data/poopsky/worldgen/world_preset/poopsky.json +++ b/src/main/resources/data/poopsky/worldgen/world_preset/poopsky.json @@ -10,8 +10,11 @@ }, "settings": "minecraft:overworld", "allowed_structure_sets": [ - "minecraft:villages", - "minecraft:strongholds" + "minecraft:pillager_outpost", + "minecraft:swamp_hut", + "minecraft:monument", + "minecraft:strongholds", + "poopsky:poop_island" ] } },