947 lines
34 KiB
Java
947 lines
34 KiB
Java
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<DataDrivenItemDefinition> 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);
|
|
}
|
|
}
|
|
}
|