package com.tiedup.remake.worldgen; import com.tiedup.remake.blocks.ModBlocks; import com.tiedup.remake.blocks.entity.TrappedChestBlockEntity; import com.tiedup.remake.core.TiedUpMod; import com.tiedup.remake.v2.V2Blocks; import com.tiedup.remake.v2.BodyRegionV2; import com.tiedup.remake.v2.bondage.datadriven.DataDrivenBondageItem; import com.tiedup.remake.v2.bondage.datadriven.DataDrivenItemDefinition; import com.tiedup.remake.v2.bondage.datadriven.DataDrivenItemRegistry; import com.tiedup.remake.v2.blocks.PetCageBlock; import com.tiedup.remake.v2.blocks.PetCagePartBlock; import java.util.List; import java.util.stream.Collectors; import javax.annotation.Nullable; import net.minecraft.core.BlockPos; import net.minecraft.core.Direction; import net.minecraft.nbt.CompoundTag; import net.minecraft.resources.ResourceLocation; import net.minecraft.nbt.DoubleTag; import net.minecraft.nbt.FloatTag; import net.minecraft.nbt.ListTag; import net.minecraft.util.RandomSource; import net.minecraft.world.item.ItemStack; import net.minecraft.world.level.ChunkPos; import net.minecraft.world.level.WorldGenLevel; import net.minecraft.world.level.block.Blocks; import net.minecraft.world.level.block.ChestBlock; import net.minecraft.world.level.block.LanternBlock; import net.minecraft.world.level.block.entity.BlockEntity; import net.minecraft.world.level.block.entity.RandomizableContainerBlockEntity; import net.minecraft.world.level.block.state.BlockState; import net.minecraft.world.level.chunk.ChunkAccess; import net.minecraft.world.level.chunk.ProtoChunk; import net.minecraft.world.level.levelgen.structure.BoundingBox; import net.minecraft.world.level.levelgen.structure.StructurePiece; import net.minecraft.world.level.levelgen.structure.pieces.StructurePieceSerializationContext; import net.minecraft.world.level.storage.loot.BuiltInLootTables; /** * Structure piece for the hanging cage. * * Scans the cave geometry in postProcess() and places: * - Iron bars column from ceiling down to the top of the cage * - A Pet Cage (3x3x2 multi-block) suspended in the cave * - A Damsel entity inside the cage (25% shiny) */ public class HangingCagePiece extends StructurePiece { private final BlockPos candidatePos; private final Direction facing; private boolean placed = false; /** Minimum air gap between cave floor and bottom of cage. */ private static final int MIN_GAP = 4; /** Height of the cage in blocks. */ private static final int CAGE_HEIGHT = 2; /** Minimum cave height: gap + cage + at least 1 iron bar. */ private static final int MIN_CAVE_HEIGHT = MIN_GAP + CAGE_HEIGHT + 1; /** Max scan distance up/down from candidate pos. */ private static final int SCAN_RANGE = 64; /** X/Z offsets to try around chunk center. Cage is 3x3 so we space by 4 to avoid overlap. */ private static final int[][] COLUMN_OFFSETS = { { 0, 0 }, { 4, 0 }, { -4, 0 }, { 0, 4 }, { 0, -4 }, { 4, 4 }, { -4, 4 }, { 4, -4 }, { -4, -4 }, { 2, 2 }, { -2, 2 }, { 2, -2 }, { -2, -2 }, }; public HangingCagePiece(BlockPos candidatePos, RandomSource random) { super( ModStructures.HANGING_CAGE_PIECE.get(), 0, makeBoundingBox(candidatePos) ); this.candidatePos = candidatePos; this.facing = Direction.Plane.HORIZONTAL.getRandomDirection(random); } public HangingCagePiece(CompoundTag tag) { super(ModStructures.HANGING_CAGE_PIECE.get(), tag); this.candidatePos = new BlockPos( tag.getInt("CandX"), tag.getInt("CandY"), tag.getInt("CandZ") ); this.facing = Direction.from2DDataValue(tag.getInt("Facing")); } private static BoundingBox makeBoundingBox(BlockPos pos) { // Generous bounding box: ±7 covers 13-wide room (±6) + column offsets, vertical covers scan + room return new BoundingBox( pos.getX() - 7, pos.getY() - SCAN_RANGE, pos.getZ() - 7, pos.getX() + 7, pos.getY() + SCAN_RANGE, pos.getZ() + 7 ); } @Override protected void addAdditionalSaveData( StructurePieceSerializationContext context, CompoundTag tag ) { tag.putInt("CandX", candidatePos.getX()); tag.putInt("CandY", candidatePos.getY()); tag.putInt("CandZ", candidatePos.getZ()); tag.putInt("Facing", facing.get2DDataValue()); } @Override public void postProcess( WorldGenLevel level, net.minecraft.world.level.StructureManager structureManager, net.minecraft.world.level.chunk.ChunkGenerator chunkGenerator, RandomSource random, BoundingBox chunkBB, ChunkPos chunkPos, BlockPos pivot ) { if (placed) return; // Try multiple columns around chunk center to maximize chance of finding a cave. // A single column has low odds of hitting a cave; 13 columns covers a wide area. for (int[] offset : COLUMN_OFFSETS) { int x = candidatePos.getX() + offset[0]; int z = candidatePos.getZ() + offset[1]; if (scanColumnAndPlace(level, random, x, z, chunkBB)) { placed = true; return; // Success } } // Fallback: carve a dungeon room and place the cage inside carveAndPlaceRoom( level, random, candidatePos.getX(), candidatePos.getZ(), chunkBB ); placed = true; } /** * Scan a single column from Y=50 down to Y=-50, looking for caves tall enough. * @return true if a cage was placed */ private boolean scanColumnAndPlace( WorldGenLevel level, RandomSource random, int x, int z, BoundingBox chunkBB ) { int scanTop = 50; int scanBottom = -50; int ceilingY = -1; boolean inAir = false; for (int y = scanTop; y >= scanBottom; y--) { BlockState state = level.getBlockState(new BlockPos(x, y, z)); boolean solid = !state.isAir() && !state.liquid(); if (!inAir && !solid) { // Transition solid -> air: ceiling is at y+1 ceilingY = y + 1; inAir = true; } else if (inAir && solid) { // Transition air -> solid: floor is at y int floorY = y; inAir = false; int caveHeight = ceilingY - floorY - 1; if (caveHeight >= MIN_CAVE_HEIGHT) { if ( tryPlaceCage( level, random, x, z, floorY, ceilingY, chunkBB ) ) { return true; } } ceilingY = -1; } } return false; } /** * Attempt to place the cage in a cave with the given floor and ceiling Y. * @return true if placement succeeded */ private boolean tryPlaceCage( WorldGenLevel level, RandomSource random, int x, int z, int floorY, int ceilingY, BoundingBox chunkBB ) { int cageMasterY = floorY + 1 + MIN_GAP; int cageTopY = cageMasterY + CAGE_HEIGHT; int numBars = ceilingY - cageTopY; if (numBars < 1) return false; BlockPos masterPos = new BlockPos(x, cageMasterY, z); // Validate 3x3x2 footprint is all air if (!level.getBlockState(masterPos).isAir()) return false; BlockPos[] partPositions = PetCageBlock.getPartPositions( masterPos, facing ); for (BlockPos partPos : partPositions) { if (!level.getBlockState(partPos).isAir()) return false; } // Place iron bars from ceiling down to top of cage for (int y = cageTopY; y < ceilingY; y++) { safeSetBlock( level, new BlockPos(x, y, z), Blocks.IRON_BARS.defaultBlockState(), chunkBB ); } // Place Pet Cage master block BlockState masterState = V2Blocks.PET_CAGE.get() .defaultBlockState() .setValue(PetCageBlock.FACING, facing); safeSetBlock(level, masterPos, masterState, chunkBB); // Place Pet Cage part blocks BlockState partState = V2Blocks.PET_CAGE_PART.get() .defaultBlockState() .setValue(PetCagePartBlock.FACING, facing); for (BlockPos partPos : partPositions) { safeSetBlock(level, partPos, partState, chunkBB); } if (chunkBB.isInside(masterPos)) { scheduleDamselSpawn(level, masterPos, random); } TiedUpMod.LOGGER.info( "[HangingCage] Placed cage in cave at {}", masterPos.toShortString() ); return true; } // RoomLayout and RoomTheme extracted to separate files in this package. // ── Fallback room generation ───────────────────────────────────── /** * Fallback: carve a 13x13x12 dungeon room and place the cage inside. * Randomly selects a data-driven theme and layout (Square/Octagonal). */ private void carveAndPlaceRoom( WorldGenLevel level, RandomSource random, int centerX, int centerZ, BoundingBox chunkBB ) { RoomThemeDefinition theme = RoomThemeRegistry.pickRandomOrFallback(random); RoomLayout layout = RoomLayout.values()[random.nextInt( RoomLayout.values().length )]; int ROOM = 13; int HEIGHT = 12; int baseX = centerX - 6; int baseZ = centerZ - 6; int floorY = candidatePos.getY() - 5; for (int rx = 0; rx < ROOM; rx++) { for (int rz = 0; rz < ROOM; rz++) { if (!layout.isInShape(rx, rz)) continue; for (int ry = 0; ry < HEIGHT; ry++) { BlockPos pos = new BlockPos( baseX + rx, floorY + ry, baseZ + rz ); boolean isWall = layout.isWall(rx, rz); boolean isFloor = ry == 0; boolean isCeiling = ry == 11; if (isFloor) { if (isWall) { safeSetBlock( level, pos, theme.wallShellBlock(), chunkBB ); } else { boolean isEdge = layout.isWallAdjacent(rx, rz); safeSetBlock( level, pos, theme.floorBlock(random, rx, rz, isEdge), chunkBB ); } } else if (isCeiling) { safeSetBlock( level, pos, theme.ceilingBlock(random), chunkBB ); } else if (isWall) { safeSetBlock( level, pos, theme.wallBlock(random, ry), chunkBB ); } else { safeSetBlock( level, pos, Blocks.AIR.defaultBlockState(), chunkBB ); } } } } placeThemeDecorations( level, random, baseX, baseZ, floorY, layout, chunkBB, theme ); placeSharedPillars( level, random, baseX, baseZ, floorY, layout, theme, chunkBB ); placeSharedFloorScatter( level, random, baseX, baseZ, floorY, layout, theme, chunkBB ); placeSharedCeilingDecor( level, random, baseX, baseZ, floorY, layout, chunkBB ); placeSharedWallLighting( level, random, baseX, baseZ, floorY, layout, chunkBB ); placeSharedWallBands( level, baseX, baseZ, floorY, layout, theme, chunkBB ); placeVanillaChest(level, random, baseX, baseZ, floorY, layout, chunkBB); placeTrappedChest(level, random, baseX, baseZ, floorY, layout, chunkBB); for (int ry = 7; ry <= 10; ry++) { safeSetBlock( level, new BlockPos(centerX, floorY + ry, centerZ), Blocks.IRON_BARS.defaultBlockState(), chunkBB ); } BlockPos masterPos = new BlockPos(centerX, floorY + 5, centerZ); BlockState masterState = V2Blocks.PET_CAGE.get() .defaultBlockState() .setValue(PetCageBlock.FACING, facing); safeSetBlock(level, masterPos, masterState, chunkBB); BlockState partState = V2Blocks.PET_CAGE_PART.get() .defaultBlockState() .setValue(PetCagePartBlock.FACING, facing); for (BlockPos partPos : PetCageBlock.getPartPositions( masterPos, facing )) { safeSetBlock(level, partPos, partState, chunkBB); } if (chunkBB.isInside(masterPos)) { scheduleDamselSpawn(level, masterPos, random); } TiedUpMod.LOGGER.info( "[HangingCage] Placed cage in carved room (theme={}, layout={}) at {}", theme.id(), layout.name(), masterPos.toShortString() ); } /** * Place a vanilla dungeon chest in the 3rd inner corner. * Uses BuiltInLootTables.SIMPLE_DUNGEON for standard dungeon loot. */ private void placeVanillaChest( WorldGenLevel level, RandomSource random, int baseX, int baseZ, int floorY, RoomLayout layout, BoundingBox chunkBB ) { int[][] corners = layout.innerCorners(); if (corners.length < 3) return; int[] c = corners[2]; if (!layout.isInShape(c[0], c[1]) || layout.isWall(c[0], c[1])) return; BlockPos chestPos = new BlockPos( baseX + c[0], floorY + 1, baseZ + c[1] ); if (!chunkBB.isInside(chestPos)) return; // Face chest toward room center Direction chestFacing; if (c[0] < 6) chestFacing = Direction.EAST; else if (c[0] > 6) chestFacing = Direction.WEST; else if (c[1] < 6) chestFacing = Direction.SOUTH; else chestFacing = Direction.NORTH; BlockState chestState = Blocks.CHEST.defaultBlockState().setValue( ChestBlock.FACING, chestFacing ); level.setBlock(chestPos, chestState, 2); BlockEntity be = level.getBlockEntity(chestPos); if (be instanceof RandomizableContainerBlockEntity container) { container.setLootTable( BuiltInLootTables.SIMPLE_DUNGEON, random.nextLong() ); } } /** * Place a TiedUp trapped chest in the 4th inner corner. * Filled with random bondage items (bind, gag, blindfold). */ private void placeTrappedChest( WorldGenLevel level, RandomSource random, int baseX, int baseZ, int floorY, RoomLayout layout, BoundingBox chunkBB ) { int[][] corners = layout.innerCorners(); if (corners.length < 4) return; int[] c = corners[3]; if (!layout.isInShape(c[0], c[1]) || layout.isWall(c[0], c[1])) return; BlockPos chestPos = new BlockPos( baseX + c[0], floorY + 1, baseZ + c[1] ); if (!chunkBB.isInside(chestPos)) return; // Face chest toward room center Direction chestFacing; if (c[0] < 6) chestFacing = Direction.EAST; else if (c[0] > 6) chestFacing = Direction.WEST; else if (c[1] < 6) chestFacing = Direction.SOUTH; else chestFacing = Direction.NORTH; BlockState chestState = ModBlocks.TRAPPED_CHEST.get() .defaultBlockState() .setValue(ChestBlock.FACING, chestFacing); level.setBlock(chestPos, chestState, 2); BlockEntity be = level.getBlockEntity(chestPos); if (be instanceof TrappedChestBlockEntity trappedChest) { // Random bind from data-driven ARMS items trappedChest.setBind(randomItemForRegion(random, BodyRegionV2.ARMS, FALLBACK_BINDS)); // Random gag (50% chance) if (random.nextFloat() < 0.50f) { trappedChest.setGag(randomItemForRegion(random, BodyRegionV2.MOUTH, FALLBACK_GAGS)); } // Random blindfold (30% chance) if (random.nextFloat() < 0.30f) { trappedChest.setBlindfold(randomItemForRegion(random, BodyRegionV2.EYES, FALLBACK_BLINDFOLDS)); } } } // Fallback item IDs for worldgen when DataDrivenItemRegistry is empty (race with reload) private static final ResourceLocation[] FALLBACK_BINDS = { ResourceLocation.fromNamespaceAndPath("tiedup", "ropes"), ResourceLocation.fromNamespaceAndPath("tiedup", "chain"), ResourceLocation.fromNamespaceAndPath("tiedup", "armbinder") }; private static final ResourceLocation[] FALLBACK_GAGS = { ResourceLocation.fromNamespaceAndPath("tiedup", "cloth_gag"), ResourceLocation.fromNamespaceAndPath("tiedup", "ball_gag") }; private static final ResourceLocation[] FALLBACK_BLINDFOLDS = { ResourceLocation.fromNamespaceAndPath("tiedup", "classic_blindfold") }; private static ItemStack randomItemForRegion(RandomSource random, BodyRegionV2 region, ResourceLocation[] fallbacks) { List defs = DataDrivenItemRegistry.getAll().stream() .filter(d -> d.occupiedRegions().contains(region)) .collect(Collectors.toList()); if (!defs.isEmpty()) { return DataDrivenBondageItem.createStack(defs.get(random.nextInt(defs.size())).id()); } // Fallback for worldgen race condition (registry not loaded yet) return DataDrivenBondageItem.createStack(fallbacks[random.nextInt(fallbacks.length)]); } static void safeSetBlock( WorldGenLevel level, BlockPos pos, BlockState state, BoundingBox chunkBB ) { if (chunkBB.isInside(pos)) { level.setBlock(pos, state, 2); } } /** * Write damsel entity NBT directly into the ProtoChunk, avoiding entity * construction on the worker thread (which deadlocks due to PlayerAnimator). * The entity will be created naturally when the chunk is promoted to a LevelChunk. */ private void scheduleDamselSpawn( WorldGenLevel level, BlockPos masterPos, RandomSource random ) { boolean shiny = random.nextFloat() < 0.25f; String entityId = shiny ? TiedUpMod.MOD_ID + ":damsel_shiny" : TiedUpMod.MOD_ID + ":damsel"; CompoundTag entityTag = new CompoundTag(); entityTag.putString("id", entityId); // Position: +1Y to be inside cage (above the thin 2px cage floor) ListTag posList = new ListTag(); posList.add(DoubleTag.valueOf(masterPos.getX() + 0.5)); posList.add(DoubleTag.valueOf(masterPos.getY() + 1.0)); posList.add(DoubleTag.valueOf(masterPos.getZ() + 0.5)); entityTag.put("Pos", posList); // Motion (stationary) ListTag motionList = new ListTag(); motionList.add(DoubleTag.valueOf(0.0)); motionList.add(DoubleTag.valueOf(0.0)); motionList.add(DoubleTag.valueOf(0.0)); entityTag.put("Motion", motionList); // Rotation (random yaw) ListTag rotList = new ListTag(); rotList.add(FloatTag.valueOf(random.nextFloat() * 360F)); rotList.add(FloatTag.valueOf(0.0F)); entityTag.put("Rotation", rotList); // Persistence + prevent fall death entityTag.putBoolean("PersistenceRequired", true); entityTag.putBoolean("OnGround", true); entityTag.putFloat("FallDistance", 0.0F); entityTag.putFloat("AbsorptionAmount", 20.0F); entityTag.putUUID("UUID", java.util.UUID.randomUUID()); // Random bind item — the damsel spawns already restrained ItemStack bindStack = randomItemForRegion(random, BodyRegionV2.ARMS, FALLBACK_BINDS); bindStack.getOrCreateTag().putString("bindMode", "full"); entityTag.put("Bind", bindStack.save(new CompoundTag())); // Add directly to chunk's pending entity list ChunkAccess chunk = level.getChunk(masterPos); if (chunk instanceof ProtoChunk protoChunk) { protoChunk.addEntity(entityTag); TiedUpMod.LOGGER.info( "[HangingCage] Scheduled {} damsel at {}", shiny ? "shiny" : "regular", masterPos.toShortString() ); } } // ── Data-driven theme decorations ─────────────────────────────── /** * Place decorations from the data-driven {@link DecorationConfig}. * Replaces the old per-enum {@code RoomTheme.placeDecorations()} method. */ private static void placeThemeDecorations( WorldGenLevel level, RandomSource random, int baseX, int baseZ, int floorY, RoomLayout layout, BoundingBox chunkBB, RoomThemeDefinition theme ) { DecorationConfig deco = theme.decorations(); int[][] corners = layout.innerCorners(); // Corner decorations (e.g. cobwebs low+high, soul fire, snow layers) // Note: only y_offset is used here — x/z offsets are ignored because corners // are placed at all 4 inner corners and direction logic would differ per corner. for (DecorationConfig.PositionedBlock pb : deco.cornerDecorations()) { for (int[] c : corners) { if (layout.isInShape(c[0], c[1]) && !layout.isWall(c[0], c[1])) { safeSetBlock(level, new BlockPos(baseX + c[0], floorY + pb.yOffset(), baseZ + c[1]), pb.state(), chunkBB); } } } // Wall midpoint decorations (e.g. soul lanterns, crying obsidian, sculk veins) int[][] wallMidpoints = {{6, 1}, {6, 11}, {1, 6}, {11, 6}}; for (DecorationConfig.PositionedBlock pb : deco.wallMidpointBlocks()) { for (int[] wp : wallMidpoints) { if (layout.isWall(wp[0], wp[1])) { safeSetBlock(level, new BlockPos(baseX + wp[0], floorY + pb.yOffset(), baseZ + wp[1]), pb.state(), chunkBB); } } } // First corner special (e.g. water cauldron, skull, TNT, sculk catalyst) // x/z offsets are multiplied by inward direction (toward room center) if (deco.firstCornerSpecial() != null && corners.length > 0) { int[] sc = corners[0]; int dirX = sc[0] < 6 ? 1 : -1; int dirZ = sc[1] < 6 ? 1 : -1; int scx = sc[0] + deco.firstCornerSpecial().xOffset() * dirX; int scz = sc[1] + deco.firstCornerSpecial().zOffset() * dirZ; if (layout.isInShape(scx, scz) && !layout.isWall(scx, scz)) { safeSetBlock(level, new BlockPos(baseX + scx, floorY + deco.firstCornerSpecial().yOffset(), baseZ + scz), deco.firstCornerSpecial().state(), chunkBB); } } // Furniture cluster at corners[1] — each item has x/z offsets relative to the // base furniture position. x_offset/z_offset values are multiplied by the // inward direction (toward room center) to handle all 4 corner orientations. if (corners.length > 1) { int[] fc = corners[1]; int dirX = fc[0] < 6 ? 1 : -1; int dirZ = fc[1] < 6 ? 1 : -1; int fcx = fc[0] + dirX; int fcz = fc[1] + dirZ; for (DecorationConfig.PositionedBlock pb : deco.furnitureCluster()) { int px = fcx + pb.xOffset() * dirX; int pz = fcz + pb.zOffset() * dirZ; if (layout.isInShape(px, pz) && !layout.isWall(px, pz)) { safeSetBlock(level, new BlockPos(baseX + px, floorY + pb.yOffset(), baseZ + pz), pb.state(), chunkBB); } } // Ceiling chain above furniture if (deco.hasCeilingChain()) { for (int cy = 8; cy <= 10; cy++) { safeSetBlock(level, new BlockPos(baseX + fcx, floorY + cy, baseZ + fcz), Blocks.CHAIN.defaultBlockState(), chunkBB); } } } // Torch lighting (wall torches at outer wall midpoints) -- used by Crypt, Sandstone if (deco.useTorchLighting()) { for (int[] wallPos : new int[][] {{6, 0}, {6, 12}, {0, 6}, {12, 6}}) { if (layout.isWall(wallPos[0], wallPos[1])) { Direction torchDir = getTorchDirection(wallPos[0], wallPos[1]); if (torchDir != null) { safeSetBlock(level, new BlockPos(baseX + wallPos[0], floorY + 3, baseZ + wallPos[1]), Blocks.WALL_TORCH.defaultBlockState().setValue( net.minecraft.world.level.block.WallTorchBlock.FACING, torchDir), chunkBB); } } } } // Side chains + hanging lanterns (shared by all themes) placeSharedChains(level, baseX, baseZ, floorY, chunkBB); placeSharedHangingLanterns(level, baseX, baseZ, floorY, chunkBB); } // ── Shared structural features (moved from RoomTheme) ────────── /** Pillar positions -- verified to be inside all 4 layouts. */ private static final int[][] PILLAR_POSITIONS = { {4, 4}, {4, 8}, {8, 4}, {8, 8}, }; /** Place 4 full-height pillars at verified positions. */ private static void placeSharedPillars( WorldGenLevel level, RandomSource random, int baseX, int baseZ, int floorY, RoomLayout layout, RoomThemeDefinition theme, BoundingBox chunkBB ) { for (int[] p : PILLAR_POSITIONS) { if (!layout.isInShape(p[0], p[1]) || layout.isWall(p[0], p[1])) continue; for (int ry = 1; ry <= 10; ry++) { safeSetBlock(level, new BlockPos(baseX + p[0], floorY + ry, baseZ + p[1]), theme.pillarBlock(random, ry), chunkBB); } } } /** Place random floor scatter (~12% of interior positions). */ private static void placeSharedFloorScatter( WorldGenLevel level, RandomSource random, int baseX, int baseZ, int floorY, RoomLayout layout, RoomThemeDefinition theme, BoundingBox chunkBB ) { for (int rx = 1; rx <= 11; rx++) { for (int rz = 1; rz <= 11; rz++) { if (!layout.isInShape(rx, rz) || layout.isWall(rx, rz)) continue; // Skip pillar positions if ((rx == 4 || rx == 8) && (rz == 4 || rz == 8)) continue; // Skip cage center area (5-7, 5-7) if (rx >= 5 && rx <= 7 && rz >= 5 && rz <= 7) continue; // Skip corner positions (used by decorations/chests) if ((rx <= 2 || rx >= 10) && (rz <= 2 || rz >= 10)) continue; if (random.nextFloat() < 0.12f) { BlockState scatter = theme.scatterBlock(random); if (scatter != null) { safeSetBlock(level, new BlockPos(baseX + rx, floorY + 1, baseZ + rz), scatter, chunkBB); } } } } } /** Place ceiling cobwebs and extra hanging chains. */ private static void placeSharedCeilingDecor( WorldGenLevel level, RandomSource random, int baseX, int baseZ, int floorY, RoomLayout layout, BoundingBox chunkBB ) { // Cobwebs along walls at ceiling level int[][] cobwebCandidates = { {2, 1}, {4, 1}, {8, 1}, {10, 1}, {2, 11}, {4, 11}, {8, 11}, {10, 11}, {1, 4}, {1, 8}, {11, 4}, {11, 8}, }; for (int[] pos : cobwebCandidates) { if (layout.isInShape(pos[0], pos[1]) && !layout.isWall(pos[0], pos[1]) && random.nextFloat() < 0.45f) { safeSetBlock(level, new BlockPos(baseX + pos[0], floorY + 10, baseZ + pos[1]), Blocks.COBWEB.defaultBlockState(), chunkBB); } } // Extra hanging chains at random interior positions int[][] chainCandidates = {{5, 3}, {7, 9}, {3, 7}}; for (int[] pos : chainCandidates) { if (layout.isInShape(pos[0], pos[1]) && !layout.isWall(pos[0], pos[1]) && random.nextFloat() < 0.6f) { safeSetBlock(level, new BlockPos(baseX + pos[0], floorY + 10, baseZ + pos[1]), Blocks.CHAIN.defaultBlockState(), chunkBB); safeSetBlock(level, new BlockPos(baseX + pos[0], floorY + 9, baseZ + pos[1]), Blocks.CHAIN.defaultBlockState(), chunkBB); } } } /** Place additional wall-mounted lighting. */ private static void placeSharedWallLighting( WorldGenLevel level, RandomSource random, int baseX, int baseZ, int floorY, RoomLayout layout, BoundingBox chunkBB ) { // Lanterns on pillar sides (facing center) at ry=1 (on the floor) int[][] pillarLanternPositions = { {5, 4}, {4, 7}, {8, 5}, {7, 8}, }; for (int[] pos : pillarLanternPositions) { if (layout.isInShape(pos[0], pos[1]) && !layout.isWall(pos[0], pos[1])) { safeSetBlock(level, new BlockPos(baseX + pos[0], floorY + 1, baseZ + pos[1]), Blocks.LANTERN.defaultBlockState(), chunkBB); } } // Extra wall sconces at quarter-points int[][] wallSconces = { {4, 0}, {8, 0}, {4, 12}, {8, 12}, {0, 4}, {0, 8}, {12, 4}, {12, 8}, }; for (int[] pos : wallSconces) { if (layout.isWall(pos[0], pos[1]) && random.nextFloat() < 0.5f) { Direction torchDir = getTorchDirection(pos[0], pos[1]); if (torchDir != null) { safeSetBlock(level, new BlockPos(baseX + pos[0], floorY + 3, baseZ + pos[1]), Blocks.WALL_TORCH.defaultBlockState().setValue( net.minecraft.world.level.block.WallTorchBlock.FACING, torchDir), chunkBB); } } } } /** Place wall accent bands at ry=5 and ry=8. */ private static void placeSharedWallBands( WorldGenLevel level, int baseX, int baseZ, int floorY, RoomLayout layout, RoomThemeDefinition theme, BoundingBox chunkBB ) { int[][] bandPositions = { {6, 1}, {6, 11}, {1, 6}, {11, 6}, {4, 1}, {8, 1}, {4, 11}, {8, 11}, {1, 4}, {1, 8}, {11, 4}, {11, 8}, }; for (int[] pos : bandPositions) { if (layout.isWall(pos[0], pos[1])) { // Band at ry=5 safeSetBlock(level, new BlockPos(baseX + pos[0], floorY + 5, baseZ + pos[1]), theme.wallAccentBlock(), chunkBB); // Optional band at ry=8 safeSetBlock(level, new BlockPos(baseX + pos[0], floorY + 8, baseZ + pos[1]), theme.wallAccentBlock(), chunkBB); } } } /** Chains on cage flanks and ceiling corners -- shared by all themes. */ private static void placeSharedChains( WorldGenLevel level, int baseX, int baseZ, int floorY, BoundingBox chunkBB ) { for (int[] chainPos : new int[][] {{3, 6}, {9, 6}}) { for (int ry = 5; ry <= 10; ry++) { safeSetBlock(level, new BlockPos(baseX + chainPos[0], floorY + ry, baseZ + chainPos[1]), Blocks.CHAIN.defaultBlockState(), chunkBB); } } for (int[] chainPos : new int[][] {{3, 3}, {3, 9}, {9, 3}, {9, 9}}) { safeSetBlock(level, new BlockPos(baseX + chainPos[0], floorY + 10, baseZ + chainPos[1]), Blocks.CHAIN.defaultBlockState(), chunkBB); } } /** Determine torch facing direction for a wall position (torch faces inward). */ private static Direction getTorchDirection(int rx, int rz) { if (rz == 0) return Direction.SOUTH; if (rz == 12) return Direction.NORTH; if (rx == 0) return Direction.EAST; if (rx == 12) return Direction.WEST; return null; } /** Hanging lanterns -- shared by all themes. */ private static void placeSharedHangingLanterns( WorldGenLevel level, int baseX, int baseZ, int floorY, BoundingBox chunkBB ) { for (int[] pos : new int[][] {{3, 3}, {9, 9}}) { safeSetBlock(level, new BlockPos(baseX + pos[0], floorY + 9, baseZ + pos[1]), Blocks.LANTERN.defaultBlockState().setValue(LanternBlock.HANGING, true), chunkBB); } } }