diff --git a/src/generated/resources/assets/mia/blockstates/abyss_spawner.json b/src/generated/resources/assets/mia/blockstates/abyss_spawner.json index 64ab0e4d..7cec4ce3 100644 --- a/src/generated/resources/assets/mia/blockstates/abyss_spawner.json +++ b/src/generated/resources/assets/mia/blockstates/abyss_spawner.json @@ -16,7 +16,7 @@ "model": "mia:block/abyss_spawner_active" }, "ominous=false,trial_spawner_state=waiting_for_reward_ejection": { - "model": "mia:block/abyss_spawner_active" + "model": "mia:block/abyss_spawner_ejecting_reward" }, "ominous=true,trial_spawner_state=active": { "model": "mia:block/abyss_spawner_active_ominous" @@ -34,7 +34,7 @@ "model": "mia:block/abyss_spawner_active_ominous" }, "ominous=true,trial_spawner_state=waiting_for_reward_ejection": { - "model": "mia:block/abyss_spawner_active_ominous" + "model": "mia:block/abyss_spawner_ejecting_reward_ominous" } } } \ No newline at end of file diff --git a/src/generated/resources/data/mia/mia/trial_spawner/example_boss.json b/src/generated/resources/data/mia/mia/trial_spawner/example_boss.json new file mode 100644 index 00000000..7ed07294 --- /dev/null +++ b/src/generated/resources/data/mia/mia/trial_spawner/example_boss.json @@ -0,0 +1,22 @@ +{ + "base_mobs": 1, + "entity_tables": [ + { + "entity": "minecraft:warden", + "weight": 1 + } + ], + "loot_tables": [ + { + "loot_table": "minecraft:chests/trial_chambers/reward_unique", + "weight": 10 + }, + { + "loot_table": "minecraft:chests/trial_chambers/reward_ominous_rare", + "weight": 5 + } + ], + "mobs_per_player": 0, + "spawn_per_tick": 1, + "spawn_range": 8 +} \ No newline at end of file diff --git a/src/generated/resources/data/mia/mia/trial_spawner/example_skeleton.json b/src/generated/resources/data/mia/mia/trial_spawner/example_skeleton.json new file mode 100644 index 00000000..c6d8fbbc --- /dev/null +++ b/src/generated/resources/data/mia/mia/trial_spawner/example_skeleton.json @@ -0,0 +1,30 @@ +{ + "base_mobs": 2, + "entity_tables": [ + { + "entity": "minecraft:skeleton", + "weight": 10 + }, + { + "entity": "minecraft:stray", + "weight": 5 + }, + { + "entity": "minecraft:bogged", + "weight": 2 + } + ], + "loot_tables": [ + { + "loot_table": "minecraft:chests/trial_chambers/reward_common", + "weight": 10 + }, + { + "loot_table": "minecraft:chests/trial_chambers/reward_rare", + "weight": 5 + } + ], + "mobs_per_player": 1, + "spawn_per_tick": 1, + "spawn_range": 5 +} \ No newline at end of file diff --git a/src/generated/resources/data/mia/mia/trial_spawner/example_spider.json b/src/generated/resources/data/mia/mia/trial_spawner/example_spider.json new file mode 100644 index 00000000..425df6be --- /dev/null +++ b/src/generated/resources/data/mia/mia/trial_spawner/example_spider.json @@ -0,0 +1,22 @@ +{ + "base_mobs": 4, + "entity_tables": [ + { + "entity": "minecraft:spider", + "weight": 10 + }, + { + "entity": "minecraft:cave_spider", + "weight": 5 + } + ], + "loot_tables": [ + { + "loot_table": "minecraft:chests/trial_chambers/reward_common", + "weight": 10 + } + ], + "mobs_per_player": 2, + "spawn_per_tick": 2, + "spawn_range": 6 +} \ No newline at end of file diff --git a/src/generated/resources/data/mia/mia/trial_spawner/example_zombie.json b/src/generated/resources/data/mia/mia/trial_spawner/example_zombie.json new file mode 100644 index 00000000..09a43f1f --- /dev/null +++ b/src/generated/resources/data/mia/mia/trial_spawner/example_zombie.json @@ -0,0 +1,34 @@ +{ + "base_mobs": 3, + "entity_tables": [ + { + "entity": "minecraft:zombie", + "weight": 10 + }, + { + "entity": "minecraft:husk", + "weight": 5 + }, + { + "entity": "minecraft:drowned", + "weight": 3 + } + ], + "loot_tables": [ + { + "loot_table": "minecraft:chests/trial_chambers/reward_common", + "weight": 10 + }, + { + "loot_table": "minecraft:chests/trial_chambers/reward_rare", + "weight": 3 + }, + { + "loot_table": "minecraft:chests/trial_chambers/reward_unique", + "weight": 1 + } + ], + "mobs_per_player": 2, + "spawn_per_tick": 1, + "spawn_range": 4 +} \ No newline at end of file diff --git a/src/main/java/com/altnoir/mia/MIA.java b/src/main/java/com/altnoir/mia/MIA.java index 29db7935..c1205b3c 100644 --- a/src/main/java/com/altnoir/mia/MIA.java +++ b/src/main/java/com/altnoir/mia/MIA.java @@ -1,6 +1,8 @@ package com.altnoir.mia; import com.altnoir.mia.core.curse.CurseManager; +import com.altnoir.mia.core.spawner.AbyssTrialSpawnerManager; +import com.altnoir.mia.core.spawner.records.AbyssTrialSpawnerPattern; import com.altnoir.mia.init.*; import com.altnoir.mia.init.event.EventHandle; import com.altnoir.mia.init.worldgen.MiaBiomeSources; @@ -28,6 +30,7 @@ public class MIA { public static final Logger LOGGER = LogUtils.getLogger(); public static final CurseManager CURSE_MANAGER = new CurseManager(); + public static final AbyssTrialSpawnerManager SPAWNER_MANAGER = new AbyssTrialSpawnerManager(); public MIA(IEventBus modEventBus, ModContainer modContainer) { modEventBus.addListener(this::commonSetup); @@ -79,6 +82,7 @@ public void onServerStarting(ServerStartingEvent event) { @SubscribeEvent private void reload(final AddReloadListenerEvent event) { event.addListener(CURSE_MANAGER); + event.addListener(SPAWNER_MANAGER); } // You can use EventBusSubscriber to automatically register all static methods diff --git a/src/main/java/com/altnoir/mia/block/AbyssSpawnerBlock.java b/src/main/java/com/altnoir/mia/block/AbyssSpawnerBlock.java index 676f7f18..46d5a7f6 100644 --- a/src/main/java/com/altnoir/mia/block/AbyssSpawnerBlock.java +++ b/src/main/java/com/altnoir/mia/block/AbyssSpawnerBlock.java @@ -80,6 +80,12 @@ public BlockEntityTicker getTicker(Level level, Block @Override public void appendHoverText(ItemStack itemStack, Item.TooltipContext context, List components, TooltipFlag flag) { super.appendHoverText(itemStack, context, components, flag); - Spawner.appendHoverText(itemStack, components, "spawn_data"); + if (itemStack.has(net.minecraft.core.component.DataComponents.BLOCK_ENTITY_DATA)) { + var beData = itemStack.get(net.minecraft.core.component.DataComponents.BLOCK_ENTITY_DATA); + if (beData != null && beData.copyTag().contains("pattern_id")) { + String patternId = beData.copyTag().getString("pattern_id"); + components.add(Component.literal("Pattern: " + patternId).withStyle(net.minecraft.ChatFormatting.GRAY)); + } + } } } diff --git a/src/main/java/com/altnoir/mia/block/entity/AbyssSpawnerBlockEntity.java b/src/main/java/com/altnoir/mia/block/entity/AbyssSpawnerBlockEntity.java index 38538cf7..acafe7a3 100644 --- a/src/main/java/com/altnoir/mia/block/entity/AbyssSpawnerBlockEntity.java +++ b/src/main/java/com/altnoir/mia/block/entity/AbyssSpawnerBlockEntity.java @@ -2,42 +2,46 @@ import com.altnoir.mia.MIA; import com.altnoir.mia.block.AbyssSpawnerBlock; +import com.altnoir.mia.core.spawner.AbyssTrialSpawner; +import com.altnoir.mia.core.spawner.records.AbyssTrialSpawnerPattern; import com.altnoir.mia.init.MiaBlockEntities; import net.minecraft.core.BlockPos; import net.minecraft.core.HolderLookup; import net.minecraft.nbt.CompoundTag; -import net.minecraft.nbt.NbtOps; import net.minecraft.network.protocol.game.ClientboundBlockEntityDataPacket; -import net.minecraft.util.RandomSource; -import net.minecraft.world.entity.EntityType; +import net.minecraft.resources.ResourceLocation; import net.minecraft.world.level.Level; -import net.minecraft.world.level.Spawner; import net.minecraft.world.level.block.entity.BlockEntity; -import net.minecraft.world.level.block.entity.trialspawner.PlayerDetector; -import net.minecraft.world.level.block.entity.trialspawner.TrialSpawner; import net.minecraft.world.level.block.entity.trialspawner.TrialSpawnerState; import net.minecraft.world.level.block.state.BlockState; import net.minecraft.world.level.block.state.properties.BlockStateProperties; +import org.jetbrains.annotations.Nullable; -public class AbyssSpawnerBlockEntity extends BlockEntity implements Spawner, TrialSpawner.StateAccessor { - private TrialSpawner abyssSpawner; +public class AbyssSpawnerBlockEntity extends BlockEntity implements AbyssTrialSpawner.StateAccessor { + private final AbyssTrialSpawner abyssSpawner; + @Nullable + private ResourceLocation patternId; + @Nullable + private AbyssTrialSpawnerPattern cachedPattern; public AbyssSpawnerBlockEntity(BlockPos pos, BlockState state) { super(MiaBlockEntities.ABYSS_SPAWNER.get(), pos, state); - PlayerDetector playerdetector = PlayerDetector.NO_CREATIVE_PLAYERS; - PlayerDetector.EntitySelector playerdetector$entityselector = PlayerDetector.EntitySelector.SELECT_FROM_LEVEL; - this.abyssSpawner = new TrialSpawner(this, playerdetector, playerdetector$entityselector); + this.abyssSpawner = new AbyssTrialSpawner(this); } @Override protected void loadAdditional(CompoundTag tag, HolderLookup.Provider registries) { super.loadAdditional(tag, registries); - if (tag.contains("normal_config")) { - CompoundTag compoundtag = tag.getCompound("normal_config").copy(); - tag.put("ominous_config", compoundtag.merge(tag.getCompound("ominous_config"))); - } - this.abyssSpawner.codec().parse(NbtOps.INSTANCE, tag).resultOrPartial(MIA.LOGGER::error).ifPresent(p_311911_ -> this.abyssSpawner = p_311911_); + if (tag.contains("pattern_id", CompoundTag.TAG_STRING)) { + this.patternId = ResourceLocation.parse(tag.getString("pattern_id")); + this.refreshPattern(); + } + + if (tag.contains("abyss_spawner", CompoundTag.TAG_COMPOUND)) { + this.abyssSpawner.load(tag.getCompound("abyss_spawner")); + } + if (this.level != null) { this.markUpdated(); } @@ -46,11 +50,12 @@ protected void loadAdditional(CompoundTag tag, HolderLookup.Provider registries) @Override protected void saveAdditional(CompoundTag tag, HolderLookup.Provider registries) { super.saveAdditional(tag, registries); - this.abyssSpawner - .codec() - .encodeStart(NbtOps.INSTANCE, this.abyssSpawner) - .ifSuccess(tag1 -> tag.merge((CompoundTag) tag1)) - .ifError(error -> MIA.LOGGER.warn("Failed to encode TrialSpawner {}", error.message())); + + if (this.patternId != null) { + tag.putString("pattern_id", this.patternId.toString()); + } + + tag.put("abyss_spawner", this.abyssSpawner.save(new CompoundTag())); } public ClientboundBlockEntityDataPacket getUpdatePacket() { @@ -59,7 +64,12 @@ public ClientboundBlockEntityDataPacket getUpdatePacket() { @Override public CompoundTag getUpdateTag(HolderLookup.Provider registries) { - return this.abyssSpawner.getData().getUpdateTag(this.getBlockState().getValue(AbyssSpawnerBlock.STATE)); + var tag = new CompoundTag(); + if (this.patternId != null) { + tag.putString("pattern_id", this.patternId.toString()); + } + tag.put("abyss_spawner", this.abyssSpawner.save(new CompoundTag())); + return tag; } @Override @@ -67,13 +77,7 @@ public boolean onlyOpCanSetNbt() { return true; } - @Override - public void setEntityId(EntityType entityType, RandomSource random) { - this.abyssSpawner.getData().setEntityId(this.abyssSpawner, random, entityType); - this.setChanged(); - } - - public TrialSpawner getAbyssSpawner() { + public AbyssTrialSpawner getAbyssSpawner() { return this.abyssSpawner; } @@ -97,4 +101,38 @@ public void markUpdated() { this.level.sendBlockUpdated(this.worldPosition, this.getBlockState(), this.getBlockState(), 3); } } + + public void setPatternId(@Nullable ResourceLocation patternId) { + this.patternId = patternId; + this.refreshPattern(); + this.setChanged(); + } + + @Nullable + @Override + public ResourceLocation getPatternId() { + return this.patternId; + } + + public void refreshPattern() { + if (this.patternId != null) { + this.cachedPattern = MIA.SPAWNER_MANAGER.getPattern(this.patternId).orElse(null); + if (this.cachedPattern == null) { + MIA.LOGGER.warn("Unknown spawner pattern: {}", this.patternId); + } + } else { + this.cachedPattern = null; + } + } + + @Nullable + @Override + public AbyssTrialSpawnerPattern getPattern() { + return this.cachedPattern; + } + + @Override + public boolean hasValidPattern() { + return this.cachedPattern != null; + } } diff --git a/src/main/java/com/altnoir/mia/core/curse/CurseManager.java b/src/main/java/com/altnoir/mia/core/curse/CurseManager.java index 3c15208a..88eaf265 100644 --- a/src/main/java/com/altnoir/mia/core/curse/CurseManager.java +++ b/src/main/java/com/altnoir/mia/core/curse/CurseManager.java @@ -2,6 +2,7 @@ import com.altnoir.mia.core.curse.records.CurseDimension; import com.altnoir.mia.core.curse.records.CurseEffect; +import com.altnoir.mia.util.MiaUtil; import com.google.gson.Gson; import com.google.gson.GsonBuilder; import com.google.gson.JsonElement; @@ -15,6 +16,7 @@ import net.minecraft.server.packs.resources.SimpleJsonResourceReloadListener; import net.minecraft.util.GsonHelper; import net.minecraft.util.profiling.ProfilerFiller; +import org.jetbrains.annotations.NotNull; import org.slf4j.Logger; import java.io.IOException; @@ -34,7 +36,7 @@ public CurseManager() { } @Override - protected Map prepare(ResourceManager resourceManager, ProfilerFiller profiler) { + protected @NotNull Map prepare(ResourceManager resourceManager, @NotNull ProfilerFiller profiler) { Map result = new HashMap<>(); for (var namespace : resourceManager.getNamespaces()) { @@ -42,14 +44,10 @@ protected Map prepare(ResourceManager resourceMan var resources = resourceManager.listResources(basePath, loc -> loc.getPath().endsWith(".json")); for (Map.Entry entry : resources.entrySet()) { var fileLoc = entry.getKey(); - var path = fileLoc.getPath(); - if (!path.startsWith(basePath)) continue; - var trimmedPath = path.substring(basePath.length() + 1); - if (trimmedPath.endsWith(".json")) { - trimmedPath = trimmedPath.substring(0, trimmedPath.length() - 5); - } - var parts = trimmedPath.split("/"); - if (parts.length > 2) continue; + var parts = MiaUtil.parseResourcePath(fileLoc.getPath(), basePath); + + if (parts == null || parts.length > 2) continue; + var fixedLoc = ResourceLocation.fromNamespaceAndPath(parts[0], parts[1]); var res = entry.getValue(); try (var stream = res.open()) { @@ -65,7 +63,7 @@ protected Map prepare(ResourceManager resourceMan } @Override - protected void apply(Map resourceLocationJsonElementMap, ResourceManager resourceManager, ProfilerFiller profilerFiller) { + protected void apply(Map resourceLocationJsonElementMap, @NotNull ResourceManager resourceManager, @NotNull ProfilerFiller profilerFiller) { curseCache.clear(); LOGGER.info("Found {} curse config files.", resourceLocationJsonElementMap.size()); diff --git a/src/main/java/com/altnoir/mia/core/spawner/AbyssTrialSpawner.java b/src/main/java/com/altnoir/mia/core/spawner/AbyssTrialSpawner.java new file mode 100644 index 00000000..f89e7ff2 --- /dev/null +++ b/src/main/java/com/altnoir/mia/core/spawner/AbyssTrialSpawner.java @@ -0,0 +1,449 @@ +package com.altnoir.mia.core.spawner; + +import com.altnoir.mia.MIA; +import com.altnoir.mia.core.spawner.records.AbyssTrialSpawnerPattern; +import com.altnoir.mia.core.spawner.records.EntityTableInstance; +import com.altnoir.mia.core.spawner.records.LootTableInstance; +import net.minecraft.core.BlockPos; +import net.minecraft.core.particles.ParticleTypes; +import net.minecraft.nbt.CompoundTag; +import net.minecraft.resources.ResourceLocation; +import net.minecraft.server.level.ServerLevel; +import net.minecraft.server.level.ServerPlayer; +import net.minecraft.sounds.SoundEvents; +import net.minecraft.sounds.SoundSource; +import net.minecraft.util.RandomSource; +import net.minecraft.util.random.WeightedRandom; +import net.minecraft.world.Difficulty; +import net.minecraft.world.entity.Entity; +import net.minecraft.world.entity.EntityType; +import net.minecraft.world.entity.Mob; +import net.minecraft.world.entity.MobSpawnType; +import net.minecraft.world.entity.player.Player; +import net.minecraft.world.item.ItemStack; +import net.minecraft.world.level.Level; +import net.minecraft.world.level.SpawnData; +import net.minecraft.world.level.block.entity.trialspawner.TrialSpawnerState; +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.parameters.LootContextParamSets; +import net.minecraft.world.level.storage.loot.parameters.LootContextParams; +import net.minecraft.world.phys.AABB; +import net.minecraft.world.phys.Vec3; +import org.jetbrains.annotations.Nullable; + +import java.util.*; + +public class AbyssTrialSpawner { + private static final int PLAYER_DETECTION_RANGE = 14; + private static final int COOLDOWN_TICKS = 36000; + private static final int SPAWN_DELAY_MIN = 40; + private static final int SPAWN_DELAY_MAX = 80; + private static final int EJECT_DELAY = 8; + private static final int WAITING_FOR_EJECTION_TICKS = 40; + + private final StateAccessor stateAccessor; + private final RandomSource random = RandomSource.create(); + + private int spawnDelay = 20; + private int totalMobsToSpawn = 0; + private int mobsSpawned = 0; + private int cooldownEndsAt = 0; + private final Set trackedPlayers = new HashSet<>(); + private final Set trackedMobs = new HashSet<>(); + + private final List rewardQueue = new ArrayList<>(); + private int ejectDelay = 0; + private int waitingForEjectionTicks = 0; + + @Nullable + private Entity displayEntity; + private double spin; + private double oSpin; + + public AbyssTrialSpawner(StateAccessor stateAccessor) { + this.stateAccessor = stateAccessor; + } + + public void tickServer(ServerLevel level, BlockPos pos, boolean ominous) { + var currentState = this.stateAccessor.getState(); + + this.trackedMobs.removeIf(uuid -> { + var entity = level.getEntity(uuid); + return entity == null || !entity.isAlive() || entity.isRemoved(); + }); + + switch (currentState) { + case INACTIVE -> tickInactive(level, pos); + case WAITING_FOR_PLAYERS -> tickWaitingForPlayers(level, pos, ominous); + case ACTIVE -> tickActive(level, pos, ominous); + case WAITING_FOR_REWARD_EJECTION -> tickWaitingForRewardEjection(level, pos, ominous); + case EJECTING_REWARD -> tickEjectingReward(level, pos, ominous); + case COOLDOWN -> tickCooldown(level, pos); + } + } + + private void tickInactive(ServerLevel level, BlockPos pos) { + if (!hasValidPattern()) return; + + if (hasNearbyPlayers(level, pos)) { + this.stateAccessor.setState(level, TrialSpawnerState.WAITING_FOR_PLAYERS); + } + } + + private void tickWaitingForPlayers(ServerLevel level, BlockPos pos, boolean ominous) { + if (!hasValidPattern()) return; + + if (!hasNearbyPlayers(level, pos)) { + this.stateAccessor.setState(level, TrialSpawnerState.INACTIVE); + return; + } + + var players = getNearbyPlayers(level, pos); + if (!players.isEmpty()) { + this.trackedPlayers.clear(); + players.forEach(p -> this.trackedPlayers.add(p.getUUID())); + + var pattern = getPattern(); + this.totalMobsToSpawn = Objects.requireNonNull(pattern).baseMobs() + (players.size() * pattern.mobsPerPlayer()); + this.mobsSpawned = 0; + this.spawnDelay = SPAWN_DELAY_MIN; + + this.stateAccessor.setState(level, TrialSpawnerState.ACTIVE); + level.playSound(null, pos, SoundEvents.TRIAL_SPAWNER_OPEN_SHUTTER, SoundSource.BLOCKS, 1.0F, 1.0F); + level.gameEvent(GameEvent.BLOCK_ACTIVATE, pos, GameEvent.Context.of(level.getBlockState(pos))); + } + } + + private void tickActive(ServerLevel level, BlockPos pos, boolean ominous) { + if (!hasValidPattern()) return; + + var pattern = getPattern(); + + updateTrackedPlayers(level, pos); + + if (this.mobsSpawned >= this.totalMobsToSpawn && this.trackedMobs.isEmpty()) { + this.rewardQueue.clear(); + this.waitingForEjectionTicks = 0; + this.stateAccessor.setState(level, TrialSpawnerState.WAITING_FOR_REWARD_EJECTION); + level.playSound(null, pos, SoundEvents.TRIAL_SPAWNER_CLOSE_SHUTTER, SoundSource.BLOCKS, 1.0F, 1.0F); + return; + } + + if (this.spawnDelay > 0) { + this.spawnDelay--; + } else if (this.mobsSpawned < this.totalMobsToSpawn) { + for (int i = 0; i < Objects.requireNonNull(pattern).spawnPerTick() && this.mobsSpawned < this.totalMobsToSpawn; i++) { + if (trySpawnMob(level, pos, pattern, ominous)) { + this.mobsSpawned++; + } + } + this.spawnDelay = this.random.nextInt(SPAWN_DELAY_MAX - SPAWN_DELAY_MIN) + SPAWN_DELAY_MIN; + } + } + + private void tickWaitingForRewardEjection(ServerLevel level, BlockPos pos, boolean ominous) { + if (this.trackedPlayers.isEmpty()) { + this.stateAccessor.setState(level, TrialSpawnerState.COOLDOWN); + this.cooldownEndsAt = level.getServer().getTickCount() + COOLDOWN_TICKS; + return; + } + + if (!hasValidPattern()) { + this.stateAccessor.setState(level, TrialSpawnerState.COOLDOWN); + this.cooldownEndsAt = level.getServer().getTickCount() + COOLDOWN_TICKS; + return; + } + + if (this.rewardQueue.isEmpty() && this.waitingForEjectionTicks == 0) { + var pattern = getPattern(); + for (var playerUuid : this.trackedPlayers) { + var player = level.getPlayerByUUID(playerUuid); + if (player != null && player.isAlive()) { + collectRewardsForPlayer(level, pos, player, pattern); + } + } + this.waitingForEjectionTicks = WAITING_FOR_EJECTION_TICKS; + } + + if (this.waitingForEjectionTicks > 0) { + this.waitingForEjectionTicks--; + return; + } + + this.ejectDelay = 0; + this.stateAccessor.setState(level, TrialSpawnerState.EJECTING_REWARD); + } + + private void tickEjectingReward(ServerLevel level, BlockPos pos, boolean ominous) { + if (this.rewardQueue.isEmpty()) { + this.trackedPlayers.clear(); + this.stateAccessor.setState(level, TrialSpawnerState.COOLDOWN); + this.cooldownEndsAt = level.getServer().getTickCount() + COOLDOWN_TICKS; + return; + } + + if (this.ejectDelay > 0) { + this.ejectDelay--; + return; + } + + var stack = this.rewardQueue.remove(0); + ejectItem(level, pos, stack); + + level.playSound(null, pos, SoundEvents.TRIAL_SPAWNER_EJECT_ITEM, SoundSource.BLOCKS, 1.0F, 1.0F); + this.ejectDelay = EJECT_DELAY; + } + + private void tickCooldown(ServerLevel level, BlockPos pos) { + if (level.getServer().getTickCount() >= this.cooldownEndsAt) { + this.stateAccessor.setState(level, TrialSpawnerState.INACTIVE); + this.totalMobsToSpawn = 0; + this.mobsSpawned = 0; + } + } + + public void tickClient(Level level, BlockPos pos, boolean ominous) { + var state = this.stateAccessor.getState(); + + this.oSpin = this.spin; + this.spin = (this.spin + (double)this.getSpinningProgress(state)) % 360.0; + + if (state == TrialSpawnerState.ACTIVE) { + if (this.random.nextInt(3) == 0) { + level.addParticle( + ParticleTypes.SMALL_FLAME, + pos.getX() + 0.5 + (this.random.nextDouble() - 0.5), + pos.getY() + 0.5 + (this.random.nextDouble() - 0.5), + pos.getZ() + 0.5 + (this.random.nextDouble() - 0.5), + 0, 0.05, 0 + ); + } + } + } + + private int getSpinningProgress(TrialSpawnerState state) { + return switch (state) { + case INACTIVE -> 0; + case WAITING_FOR_PLAYERS -> 1; + case ACTIVE -> 4; + case WAITING_FOR_REWARD_EJECTION, EJECTING_REWARD -> 2; + case COOLDOWN -> 1; + }; + } + + private boolean trySpawnMob(ServerLevel level, BlockPos pos, AbyssTrialSpawnerPattern pattern, boolean ominous) { + if (level.getDifficulty() == Difficulty.PEACEFUL) { + return false; + } + + var selectedEntity = WeightedRandom.getRandomItem(this.random, pattern.entityTables().unwrap()); + if (selectedEntity.isEmpty()) { + return false; + } + + var entityType = selectedEntity.get().getEntityType(); + + int range = pattern.spawnRange(); + for (int attempts = 0; attempts < 20; attempts++) { + double x = pos.getX() + (this.random.nextDouble() - 0.5) * range * 2; + double y = pos.getY() + this.random.nextInt(3) - 1; + double z = pos.getZ() + (this.random.nextDouble() - 0.5) * range * 2; + + var spawnPos = BlockPos.containing(x, y, z); + + if (level.noCollision(entityType.getSpawnAABB(x, y, z))) { + var entity = entityType.create(level, null, spawnPos, MobSpawnType.TRIAL_SPAWNER, false, false); + if (entity != null) { + if (entity instanceof Mob mob) { + mob.finalizeSpawn(level, level.getCurrentDifficultyAt(spawnPos), MobSpawnType.TRIAL_SPAWNER, null); + } + + level.addFreshEntity(entity); + this.trackedMobs.add(entity.getUUID()); + + level.playSound(null, pos, SoundEvents.TRIAL_SPAWNER_SPAWN_MOB, SoundSource.BLOCKS, 1.0F, 1.0F); + level.gameEvent(entity, GameEvent.ENTITY_PLACE, spawnPos); + + level.sendParticles( + ParticleTypes.POOF, + x, y + 0.5, z, + 10, 0.2, 0.2, 0.2, 0.02 + ); + + return true; + } + } + } + + return false; + } + + private void collectRewardsForPlayer(ServerLevel level, BlockPos pos, Player player, AbyssTrialSpawnerPattern pattern) { + var selectedLoot = WeightedRandom.getRandomItem(this.random, pattern.lootTables().unwrap()); + if (selectedLoot.isEmpty()) { + return; + } + + var lootTable = level.getServer().reloadableRegistries().getLootTable(selectedLoot.get().getLootTableKey()); + if (lootTable == LootTable.EMPTY) { + MIA.LOGGER.warn("Loot table not found: {}", selectedLoot.get().getLootTableKey().location()); + return; + } + + var lootParams = new LootParams.Builder(level) + .withParameter(LootContextParams.ORIGIN, Vec3.atCenterOf(pos)) + .withParameter(LootContextParams.THIS_ENTITY, player) + .withLuck(player.getLuck()) + .create(LootContextParamSets.CHEST); + + this.rewardQueue.addAll(lootTable.getRandomItems(lootParams)); + } + + private void ejectItem(ServerLevel level, BlockPos pos, ItemStack stack) { + var x = pos.getX() + 0.5; + var y = pos.getY() + 1.2; + var z = pos.getZ() + 0.5; + + var angle = this.random.nextDouble() * Math.PI * 2; + var speed = 0.1 + this.random.nextDouble() * 0.05; + + var itemEntity = new net.minecraft.world.entity.item.ItemEntity( + level, x, y, z, stack, + Math.cos(angle) * speed, + 0.2 + this.random.nextDouble() * 0.1, + Math.sin(angle) * speed + ); + itemEntity.setDefaultPickUpDelay(); + level.addFreshEntity(itemEntity); + + level.sendParticles( + ParticleTypes.SMALL_FLAME, + x, y, z, + 5, 0.1, 0.1, 0.1, 0.02 + ); + } + + private boolean hasNearbyPlayers(ServerLevel level, BlockPos pos) { + return !getNearbyPlayers(level, pos).isEmpty(); + } + + private List getNearbyPlayers(ServerLevel level, BlockPos pos) { + var detectionBox = new AABB(pos).inflate(PLAYER_DETECTION_RANGE); + return level.getPlayers(player -> + !player.isCreative() && + !player.isSpectator() && + player.isAlive() && + detectionBox.contains(player.position()) + ); + } + + private void updateTrackedPlayers(ServerLevel level, BlockPos pos) { + for (var player : getNearbyPlayers(level, pos)) { + this.trackedPlayers.add(player.getUUID()); + } + + if (hasValidPattern()) { + var pattern = getPattern(); + var newTotal = Objects.requireNonNull(pattern).baseMobs() + (this.trackedPlayers.size() * pattern.mobsPerPlayer()); + if (newTotal > this.totalMobsToSpawn) { + this.totalMobsToSpawn = newTotal; + } + } + } + + private boolean hasValidPattern() { + return this.stateAccessor.hasValidPattern(); + } + + @Nullable + private AbyssTrialSpawnerPattern getPattern() { + return this.stateAccessor.getPattern(); + } + + public CompoundTag save(CompoundTag tag) { + tag.putInt("spawn_delay", this.spawnDelay); + tag.putInt("total_mobs", this.totalMobsToSpawn); + tag.putInt("mobs_spawned", this.mobsSpawned); + tag.putInt("cooldown_ends_at", this.cooldownEndsAt); + + var playersTag = new CompoundTag(); + var i = 0; + for (var uuid : this.trackedPlayers) { + playersTag.putUUID("player_" + i, uuid); + i++; + } + playersTag.putInt("count", i); + tag.put("tracked_players", playersTag); + + var mobsTag = new CompoundTag(); + i = 0; + for (var uuid : this.trackedMobs) { + mobsTag.putUUID("mob_" + i, uuid); + i++; + } + mobsTag.putInt("count", i); + tag.put("tracked_mobs", mobsTag); + + return tag; + } + + public void load(CompoundTag tag) { + this.spawnDelay = tag.getInt("spawn_delay"); + this.totalMobsToSpawn = tag.getInt("total_mobs"); + this.mobsSpawned = tag.getInt("mobs_spawned"); + this.cooldownEndsAt = tag.getInt("cooldown_ends_at"); + + this.trackedPlayers.clear(); + if (tag.contains("tracked_players", CompoundTag.TAG_COMPOUND)) { + var playersTag = tag.getCompound("tracked_players"); + var count = playersTag.getInt("count"); + for (var i = 0; i < count; i++) { + if (playersTag.hasUUID("player_" + i)) { + this.trackedPlayers.add(playersTag.getUUID("player_" + i)); + } + } + } + + this.trackedMobs.clear(); + if (tag.contains("tracked_mobs", CompoundTag.TAG_COMPOUND)) { + var mobsTag = tag.getCompound("tracked_mobs"); + var count = mobsTag.getInt("count"); + for (var i = 0; i < count; i++) { + if (mobsTag.hasUUID("mob_" + i)) { + this.trackedMobs.add(mobsTag.getUUID("mob_" + i)); + } + } + } + } + + public double getSpin() { + return this.spin; + } + + public double getOSpin() { + return this.oSpin; + } + + @Nullable + public Entity getOrCreateDisplayEntity(Level level, BlockPos pos) { + if (this.displayEntity == null && hasValidPattern() && level instanceof ServerLevel serverLevel) { + var pattern = getPattern(); + var selected = WeightedRandom.getRandomItem(this.random, pattern.entityTables().unwrap()); + if (selected.isPresent()) { + this.displayEntity = selected.get().getEntityType().create(serverLevel, null, pos, MobSpawnType.TRIAL_SPAWNER, false, false); + } + } + return this.displayEntity; + } + + public interface StateAccessor { + TrialSpawnerState getState(); + void setState(Level level, TrialSpawnerState state); + void markUpdated(); + @Nullable ResourceLocation getPatternId(); + @Nullable AbyssTrialSpawnerPattern getPattern(); + boolean hasValidPattern(); + } +} diff --git a/src/main/java/com/altnoir/mia/core/spawner/AbyssTrialSpawnerDataProvider.java b/src/main/java/com/altnoir/mia/core/spawner/AbyssTrialSpawnerDataProvider.java new file mode 100644 index 00000000..e7c07179 --- /dev/null +++ b/src/main/java/com/altnoir/mia/core/spawner/AbyssTrialSpawnerDataProvider.java @@ -0,0 +1,153 @@ +package com.altnoir.mia.core.spawner; + +import com.altnoir.mia.core.spawner.records.AbyssTrialSpawnerPattern; +import com.altnoir.mia.core.spawner.records.EntityTableInstance; +import com.altnoir.mia.core.spawner.records.LootTableInstance; +import com.google.gson.JsonArray; +import com.google.gson.JsonObject; +import net.minecraft.core.HolderLookup; +import net.minecraft.core.registries.BuiltInRegistries; +import net.minecraft.core.registries.Registries; +import net.minecraft.data.CachedOutput; +import net.minecraft.data.DataProvider; +import net.minecraft.data.PackOutput; +import net.minecraft.resources.ResourceKey; +import net.minecraft.resources.ResourceLocation; +import net.minecraft.util.random.WeightedRandomList; +import net.minecraft.world.entity.EntityType; +import net.minecraft.world.level.storage.loot.LootTable; +import org.jetbrains.annotations.NotNull; + +import java.util.ArrayList; +import java.util.List; +import java.util.concurrent.CompletableFuture; + +public abstract class AbyssTrialSpawnerDataProvider implements DataProvider { + private final String modId; + private final PackOutput output; + private final CompletableFuture registries; + private final List definitions = new ArrayList<>(); + + public AbyssTrialSpawnerDataProvider(String modId, PackOutput output, CompletableFuture registries) { + this.modId = modId; + this.output = output; + this.registries = registries; + } + + protected abstract void addSpawners(); + + protected void add(ResourceLocation id, AbyssTrialSpawnerPattern pattern) { + definitions.add(new SpawnerDefinition(id, pattern)); + } + + protected AbyssTrialSpawnerPattern createPattern( + List entities, + List loots, + int baseMobs, + int mobsPerPlayer, + int spawnPerTick, + int spawnRange) { + var entityTables = WeightedRandomList.create(entities); + var lootTables = WeightedRandomList.create(loots); + return new AbyssTrialSpawnerPattern(lootTables, entityTables, baseMobs, mobsPerPlayer, spawnPerTick, spawnRange); + } + + protected EntityTableInstance entity(EntityType entityType, int weight) { + return new EntityTableInstance(entityType, weight); + } + + protected LootTableInstance loot(ResourceKey lootTableKey, int weight) { + return new LootTableInstance(lootTableKey, weight); + } + + protected LootTableInstance loot(ResourceLocation lootTableId, int weight) { + return new LootTableInstance(ResourceKey.create(Registries.LOOT_TABLE, lootTableId), weight); + } + + protected LootTableInstance loot(String namespace, String path, int weight) { + return new LootTableInstance( + ResourceKey.create(Registries.LOOT_TABLE, ResourceLocation.fromNamespaceAndPath(namespace, path)), + weight + ); + } + + private void validate(HolderLookup.Provider registries, ResourceLocation id, AbyssTrialSpawnerPattern pattern) { + for (var entity : pattern.entityTables().unwrap()) { + var entityType = entity.getEntityType(); + var entityId = BuiltInRegistries.ENTITY_TYPE.getKey(entityType); + } + + if (pattern.baseMobs() < 0) { + throw new IllegalArgumentException("baseMobs must be >= 0 in spawner " + id); + } + if (pattern.mobsPerPlayer() < 0) { + throw new IllegalArgumentException("mobsPerPlayer must be >= 0 in spawner " + id); + } + if (pattern.spawnPerTick() <= 0) { + throw new IllegalArgumentException("spawnPerTick must be > 0 in spawner " + id); + } + if (pattern.spawnRange() <= 0) { + throw new IllegalArgumentException("spawnRange must be > 0 in spawner " + id); + } + } + + @Override + public @NotNull CompletableFuture run(@NotNull CachedOutput cachedOutput) { + return registries.thenCompose(lookup -> { + addSpawners(); + + var basePath = output.getOutputFolder().resolve("data/" + modId + "/mia/trial_spawner"); + var futures = new ArrayList>(); + + for (var def : definitions) { + validate(lookup, def.id, def.pattern); + + var root = new JsonObject(); + + if (!def.pattern.entityTables().unwrap().isEmpty()) { + var entityTablesArray = new JsonArray(); + for (var entityEntry : def.pattern.entityTables().unwrap()) { + var entityJson = new JsonObject(); + var entityId = BuiltInRegistries.ENTITY_TYPE.getKey(entityEntry.getEntityType()); + entityJson.addProperty("entity", entityId.toString()); + entityJson.addProperty("weight", entityEntry.getWeight().asInt()); + entityTablesArray.add(entityJson); + } + root.add("entity_tables", entityTablesArray); + } + + if (!def.pattern.lootTables().unwrap().isEmpty()) { + var lootTablesArray = new JsonArray(); + for (var lootEntry : def.pattern.lootTables().unwrap()) { + var lootJson = new JsonObject(); + lootJson.addProperty("loot_table", lootEntry.getLootTableKey().location().toString()); + lootJson.addProperty("weight", lootEntry.getWeight().asInt()); + lootTablesArray.add(lootJson); + } + root.add("loot_tables", lootTablesArray); + } + + root.addProperty("base_mobs", def.pattern.baseMobs()); + root.addProperty("mobs_per_player", def.pattern.mobsPerPlayer()); + root.addProperty("spawn_per_tick", def.pattern.spawnPerTick()); + root.addProperty("spawn_range", def.pattern.spawnRange()); + + var fileName = def.id.getPath() + ".json"; + var path = basePath.resolve(fileName); + + var future = DataProvider.saveStable(cachedOutput, root, path); + futures.add(future); + } + + return CompletableFuture.allOf(futures.toArray(new CompletableFuture[0])); + }); + } + + @Override + public @NotNull String getName() { + return "Abyss Trial Spawner data for " + this.modId; + } + + private record SpawnerDefinition(ResourceLocation id, AbyssTrialSpawnerPattern pattern) { + } +} diff --git a/src/main/java/com/altnoir/mia/core/spawner/AbyssTrialSpawnerManager.java b/src/main/java/com/altnoir/mia/core/spawner/AbyssTrialSpawnerManager.java new file mode 100644 index 00000000..5f5d67ff --- /dev/null +++ b/src/main/java/com/altnoir/mia/core/spawner/AbyssTrialSpawnerManager.java @@ -0,0 +1,151 @@ +package com.altnoir.mia.core.spawner; + +import com.altnoir.mia.core.spawner.records.AbyssTrialSpawnerPattern; +import com.altnoir.mia.core.spawner.records.EntityTableInstance; +import com.altnoir.mia.core.spawner.records.LootTableInstance; +import com.altnoir.mia.util.MiaUtil; +import com.google.gson.Gson; +import com.google.gson.GsonBuilder; +import com.google.gson.JsonElement; +import com.google.gson.JsonParseException; +import com.mojang.logging.LogUtils; +import net.minecraft.core.registries.BuiltInRegistries; +import net.minecraft.core.registries.Registries; +import net.minecraft.resources.ResourceKey; +import net.minecraft.resources.ResourceLocation; +import net.minecraft.server.packs.resources.Resource; +import net.minecraft.server.packs.resources.ResourceManager; +import net.minecraft.server.packs.resources.SimpleJsonResourceReloadListener; +import net.minecraft.util.GsonHelper; +import net.minecraft.util.profiling.ProfilerFiller; +import net.minecraft.util.random.WeightedRandomList; +import net.minecraft.world.level.storage.loot.LootTable; +import org.jetbrains.annotations.NotNull; +import org.slf4j.Logger; + +import java.io.IOException; +import java.io.InputStreamReader; +import java.util.*; + +public class AbyssTrialSpawnerManager extends SimpleJsonResourceReloadListener { + private static final Gson GSON = + (new GsonBuilder()).setPrettyPrinting().disableHtmlEscaping().create(); + + private static final Logger LOGGER = LogUtils.getLogger(); + + private final Map spawnerPatterns = new HashMap<>(); + + public AbyssTrialSpawnerManager() { + super(GSON, "mia/trial_spawner"); + } + + @Override + protected @NotNull Map prepare(ResourceManager resourceManager, @NotNull ProfilerFiller profiler) { + Map result = new HashMap<>(); + + for (var namespace : resourceManager.getNamespaces()) { + var basePath = "mia/trial_spawner"; + var resources = resourceManager.listResources(basePath, loc -> loc.getPath().endsWith(".json")); + + for (Map.Entry entry : resources.entrySet()) { + var fileLoc = entry.getKey(); + var parts = MiaUtil.parseResourcePath(fileLoc.getPath(), basePath); + + if (parts == null || parts.length != 1) continue; + + var fixedLoc = ResourceLocation.fromNamespaceAndPath(namespace, parts[0]); + var res = entry.getValue(); + + try (var stream = res.open(); var reader = new InputStreamReader(stream)) { + var json = GsonHelper.fromJson(GSON, reader, JsonElement.class); + result.put(fixedLoc, json); + } catch (IOException | JsonParseException e) { + LOGGER.error("Failed to load trial spawner JSON from {}", fileLoc, e); + } + } + } + + return result; + } + + @Override + protected void apply(Map resourceLocationJsonElementMap, @NotNull ResourceManager resourceManager, @NotNull ProfilerFiller profiler) { + spawnerPatterns.clear(); + + LOGGER.info("Found {} trial spawner config files.", resourceLocationJsonElementMap.size()); + + for (Map.Entry entry : resourceLocationJsonElementMap.entrySet()) { + var id = entry.getKey(); + var jsonElement = entry.getValue(); + + if (!jsonElement.isJsonObject()) { + LOGGER.warn("Trial spawner config {} is not a JSON object", id); + continue; + } + + try { + var json = jsonElement.getAsJsonObject(); + + var lootTablesList = new ArrayList(); + if (json.has("loot_tables")) { + var lootTablesArray = json.getAsJsonArray("loot_tables"); + for (var lootElement : lootTablesArray) { + var lootJson = lootElement.getAsJsonObject(); + var lootTableId = ResourceLocation.parse(lootJson.get("loot_table").getAsString()); + var weight = lootJson.get("weight").getAsInt(); + var lootTableKey = ResourceKey.create(Registries.LOOT_TABLE, lootTableId); + lootTablesList.add(new LootTableInstance(lootTableKey, weight)); + } + } + + var entityTablesList = new ArrayList(); + if (json.has("entity_tables")) { + var entityTablesArray = json.getAsJsonArray("entity_tables"); + for (var entityElement : entityTablesArray) { + var entityJson = entityElement.getAsJsonObject(); + var entityId = ResourceLocation.parse(entityJson.get("entity").getAsString()); + var weight = entityJson.get("weight").getAsInt(); + + var entityType = BuiltInRegistries.ENTITY_TYPE.get(entityId); + entityTablesList.add(new EntityTableInstance(entityType, weight)); + } + } + + var baseMobs = json.has("base_mobs") ? json.get("base_mobs").getAsInt() : 2; + var mobsPerPlayer = json.has("mobs_per_player") ? json.get("mobs_per_player").getAsInt() : 1; + var spawnPerTick = json.has("spawn_per_tick") ? json.get("spawn_per_tick").getAsInt() : 1; + var spawnRange = json.has("spawn_range") ? json.get("spawn_range").getAsInt() : 4; + + var lootTables = WeightedRandomList.create(lootTablesList); + var entityTables = WeightedRandomList.create(entityTablesList); + + var pattern = new AbyssTrialSpawnerPattern( + lootTables, + entityTables, + baseMobs, + mobsPerPlayer, + spawnPerTick, + spawnRange + ); + + spawnerPatterns.put(id, pattern); + LOGGER.info("Loaded trial spawner pattern: {}", id); + + } catch (Exception e) { + LOGGER.error("Failed to parse trial spawner config {}", id, e); + } + } + } + + public Optional getPattern(ResourceLocation id) { + return Optional.ofNullable(spawnerPatterns.get(id)); + } + + public Set getPatternIds() { + return spawnerPatterns.keySet(); + } + + public Map getPatterns() { + return Collections.unmodifiableMap(spawnerPatterns); + } +} diff --git a/src/main/java/com/altnoir/mia/core/spawner/records/AbyssTrialSpawnerPattern.java b/src/main/java/com/altnoir/mia/core/spawner/records/AbyssTrialSpawnerPattern.java new file mode 100644 index 00000000..39dbf929 --- /dev/null +++ b/src/main/java/com/altnoir/mia/core/spawner/records/AbyssTrialSpawnerPattern.java @@ -0,0 +1,12 @@ +package com.altnoir.mia.core.spawner.records; + +import net.minecraft.util.random.WeightedRandomList; + +public record AbyssTrialSpawnerPattern( + WeightedRandomList lootTables, + WeightedRandomList entityTables, + int baseMobs, + int mobsPerPlayer, + int spawnPerTick, + int spawnRange + ) { } diff --git a/src/main/java/com/altnoir/mia/core/spawner/records/EntityTableInstance.java b/src/main/java/com/altnoir/mia/core/spawner/records/EntityTableInstance.java new file mode 100644 index 00000000..199ad27f --- /dev/null +++ b/src/main/java/com/altnoir/mia/core/spawner/records/EntityTableInstance.java @@ -0,0 +1,17 @@ +package com.altnoir.mia.core.spawner.records; + +import net.minecraft.util.random.WeightedEntry; +import net.minecraft.world.entity.EntityType; + +public class EntityTableInstance extends WeightedEntry.IntrusiveBase { + private final EntityType entityType; + + public EntityTableInstance(EntityType entityType, int weight) { + super(weight); + this.entityType = entityType; + } + + public EntityType getEntityType() { + return entityType; + } +} diff --git a/src/main/java/com/altnoir/mia/core/spawner/records/LootTableInstance.java b/src/main/java/com/altnoir/mia/core/spawner/records/LootTableInstance.java new file mode 100644 index 00000000..96b83b47 --- /dev/null +++ b/src/main/java/com/altnoir/mia/core/spawner/records/LootTableInstance.java @@ -0,0 +1,19 @@ +package com.altnoir.mia.core.spawner.records; + +import net.minecraft.core.registries.Registries; +import net.minecraft.resources.ResourceKey; +import net.minecraft.util.random.WeightedEntry; +import net.minecraft.world.level.storage.loot.LootTable; + +public class LootTableInstance extends WeightedEntry.IntrusiveBase { + private final ResourceKey lootTableKey; + + public LootTableInstance(ResourceKey lootTableKey, int weight) { + super(weight); + this.lootTableKey = lootTableKey; + } + + public ResourceKey getLootTableKey() { + return lootTableKey; + } +} diff --git a/src/main/java/com/altnoir/mia/datagen/DataGenerators.java b/src/main/java/com/altnoir/mia/datagen/DataGenerators.java index 613993ec..8072aadf 100644 --- a/src/main/java/com/altnoir/mia/datagen/DataGenerators.java +++ b/src/main/java/com/altnoir/mia/datagen/DataGenerators.java @@ -35,6 +35,7 @@ public static void gatherData(GatherDataEvent event) { generators.addProvider(event.includeServer(), new MiaCuriosProvider(packOutput, existingFileHelper, lookupProvider)); generators.addProvider(event.includeServer(), new MiaCurseDataProvider(packOutput, lookupProvider)); + generators.addProvider(event.includeServer(), new MiaTrialSpawnerProvider(packOutput, lookupProvider)); generators.addProvider(event.includeClient(), new MiaBlockStateProvider(packOutput, existingFileHelper)); generators.addProvider(event.includeClient(), new MiaItemModelProvider(packOutput, existingFileHelper)); diff --git a/src/main/java/com/altnoir/mia/datagen/MiaTrialSpawnerProvider.java b/src/main/java/com/altnoir/mia/datagen/MiaTrialSpawnerProvider.java new file mode 100644 index 00000000..48a5e967 --- /dev/null +++ b/src/main/java/com/altnoir/mia/datagen/MiaTrialSpawnerProvider.java @@ -0,0 +1,94 @@ +package com.altnoir.mia.datagen; + +import com.altnoir.mia.MIA; +import com.altnoir.mia.core.spawner.AbyssTrialSpawnerDataProvider; +import net.minecraft.core.HolderLookup; +import net.minecraft.data.PackOutput; +import net.minecraft.resources.ResourceLocation; +import net.minecraft.world.entity.EntityType; +import net.minecraft.world.level.storage.loot.BuiltInLootTables; + +import java.util.List; +import java.util.concurrent.CompletableFuture; + +public class MiaTrialSpawnerProvider extends AbyssTrialSpawnerDataProvider { + public MiaTrialSpawnerProvider(PackOutput output, CompletableFuture registries) { + super(MIA.MOD_ID, output, registries); + } + + @Override + protected void addSpawners() { + add( + ResourceLocation.fromNamespaceAndPath(MIA.MOD_ID, "example_zombie"), + createPattern( + List.of( + entity(EntityType.ZOMBIE, 10), + entity(EntityType.HUSK, 5), + entity(EntityType.DROWNED, 3) + ), + List.of( + loot(BuiltInLootTables.TRIAL_CHAMBERS_REWARD_COMMON, 10), + loot(BuiltInLootTables.TRIAL_CHAMBERS_REWARD_RARE, 3), + loot(BuiltInLootTables.TRIAL_CHAMBERS_REWARD_UNIQUE, 1) + ), + 3, + 2, + 1, + 4 + ) + ); + + add( + ResourceLocation.fromNamespaceAndPath(MIA.MOD_ID, "example_skeleton"), + createPattern( + List.of( + entity(EntityType.SKELETON, 10), + entity(EntityType.STRAY, 5), + entity(EntityType.BOGGED, 2) + ), + List.of( + loot(BuiltInLootTables.TRIAL_CHAMBERS_REWARD_COMMON, 10), + loot(BuiltInLootTables.TRIAL_CHAMBERS_REWARD_RARE, 5) + ), + 2, + 1, + 1, + 5 + ) + ); + + add( + ResourceLocation.fromNamespaceAndPath(MIA.MOD_ID, "example_spider"), + createPattern( + List.of( + entity(EntityType.SPIDER, 10), + entity(EntityType.CAVE_SPIDER, 5) + ), + List.of( + loot(BuiltInLootTables.TRIAL_CHAMBERS_REWARD_COMMON, 10) + ), + 4, + 2, + 2, + 6 + ) + ); + + add( + ResourceLocation.fromNamespaceAndPath(MIA.MOD_ID, "example_boss"), + createPattern( + List.of( + entity(EntityType.WARDEN, 1) + ), + List.of( + loot(BuiltInLootTables.TRIAL_CHAMBERS_REWARD_UNIQUE, 10), + loot(BuiltInLootTables.TRIAL_CHAMBERS_REWARD_OMINOUS_RARE, 5) + ), + 1, + 0, + 1, + 8 + ) + ); + } +} diff --git a/src/main/java/com/altnoir/mia/datagen/blockstate/MiaStateProvider.java b/src/main/java/com/altnoir/mia/datagen/blockstate/MiaStateProvider.java index 8f41b3da..071420de 100644 --- a/src/main/java/com/altnoir/mia/datagen/blockstate/MiaStateProvider.java +++ b/src/main/java/com/altnoir/mia/datagen/blockstate/MiaStateProvider.java @@ -82,7 +82,7 @@ public void abyssSpawnerBlockState(BlockStateProvider p, Block block) { TrialSpawnerState.EJECTING_REWARD, "_ejecting_reward", TrialSpawnerState.INACTIVE, "", TrialSpawnerState.WAITING_FOR_PLAYERS, "_active", - TrialSpawnerState.WAITING_FOR_REWARD_EJECTION, "_active" + TrialSpawnerState.WAITING_FOR_REWARD_EJECTION, "_ejecting_reward" ); for (boolean ominous : new boolean[]{false, true}) { diff --git a/src/main/java/com/altnoir/mia/util/MiaUtil.java b/src/main/java/com/altnoir/mia/util/MiaUtil.java index 9b221f2b..2363ce18 100644 --- a/src/main/java/com/altnoir/mia/util/MiaUtil.java +++ b/src/main/java/com/altnoir/mia/util/MiaUtil.java @@ -210,4 +210,28 @@ public static float format2(double value) { public static boolean isCreativeOrSpectator(Player player) { return player.isSpectator() || player.isCreative(); } + + /** + * Parses a resource location path by trimming the base path and removing .json extension + * @param filePath The full file path from ResourceLocation + * @param basePath The base path to trim (e.g., "mia/curse") + * @return Array of path parts after processing, or null if path doesn't start with basePath + */ + public static @Nullable String[] parseResourcePath(String filePath, String basePath) { + if (!filePath.startsWith(basePath)) { + return null; + } + + // Ensure there is at least one character after basePath before substring + if (filePath.length() <= basePath.length()) { + return null; + } + + var trimmedPath = filePath.substring(basePath.length() + 1); + if (trimmedPath.endsWith(".json")) { + trimmedPath = trimmedPath.substring(0, trimmedPath.length() - 5); + } + + return trimmedPath.split("/"); + } }