Clean repo for open source release
Remove build artifacts, dev tool configs, unused dependencies, and third-party source dumps. Add proper README, update .gitignore, clean up Makefile.
This commit is contained in:
@@ -0,0 +1,66 @@
|
||||
package com.tiedup.remake.v2.blocks;
|
||||
|
||||
import javax.annotation.Nullable;
|
||||
import net.minecraft.core.BlockPos;
|
||||
import net.minecraft.nbt.CompoundTag;
|
||||
import net.minecraft.resources.ResourceLocation;
|
||||
import net.minecraft.world.level.block.entity.BlockEntity;
|
||||
import net.minecraft.world.level.block.entity.BlockEntityType;
|
||||
import net.minecraft.world.level.block.state.BlockState;
|
||||
|
||||
/**
|
||||
* Base block entity for blocks that render with OBJ models.
|
||||
* Subclasses define the model and texture locations.
|
||||
*/
|
||||
public abstract class ObjBlockEntity extends BlockEntity {
|
||||
|
||||
public ObjBlockEntity(
|
||||
BlockEntityType<?> type,
|
||||
BlockPos pos,
|
||||
BlockState state
|
||||
) {
|
||||
super(type, pos, state);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the OBJ model resource location.
|
||||
* Example: "tiedup:models/obj/blocks/bowl/model.obj"
|
||||
*/
|
||||
@Nullable
|
||||
public abstract ResourceLocation getModelLocation();
|
||||
|
||||
/**
|
||||
* Get the texture resource location.
|
||||
* Example: "tiedup:textures/block/bowl.png"
|
||||
*/
|
||||
@Nullable
|
||||
public abstract ResourceLocation getTextureLocation();
|
||||
|
||||
/**
|
||||
* Get the model scale (default 1.0).
|
||||
* Override to scale the model up or down.
|
||||
*/
|
||||
public float getModelScale() {
|
||||
return 1.0f;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the model position offset (x, y, z).
|
||||
* Override to adjust model position within the block.
|
||||
*/
|
||||
public float[] getModelOffset() {
|
||||
return new float[] { 0.0f, 0.0f, 0.0f };
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void saveAdditional(CompoundTag tag) {
|
||||
super.saveAdditional(tag);
|
||||
// Subclasses can override to save additional data
|
||||
}
|
||||
|
||||
@Override
|
||||
public void load(CompoundTag tag) {
|
||||
super.load(tag);
|
||||
// Subclasses can override to load additional data
|
||||
}
|
||||
}
|
||||
181
src/main/java/com/tiedup/remake/v2/blocks/PetBedBlock.java
Normal file
181
src/main/java/com/tiedup/remake/v2/blocks/PetBedBlock.java
Normal file
@@ -0,0 +1,181 @@
|
||||
package com.tiedup.remake.v2.blocks;
|
||||
|
||||
import com.tiedup.remake.core.TiedUpMod;
|
||||
import com.tiedup.remake.v2.V2BlockEntities;
|
||||
import net.minecraft.core.BlockPos;
|
||||
import net.minecraft.core.Direction;
|
||||
import net.minecraft.server.level.ServerLevel;
|
||||
import net.minecraft.server.level.ServerPlayer;
|
||||
import net.minecraft.world.InteractionHand;
|
||||
import net.minecraft.world.InteractionResult;
|
||||
import net.minecraft.world.entity.player.Player;
|
||||
import net.minecraft.world.item.context.BlockPlaceContext;
|
||||
import net.minecraft.world.level.BlockGetter;
|
||||
import net.minecraft.world.level.Level;
|
||||
import net.minecraft.world.level.block.Block;
|
||||
import net.minecraft.world.level.block.EntityBlock;
|
||||
import net.minecraft.world.level.block.HorizontalDirectionalBlock;
|
||||
import net.minecraft.world.level.block.RenderShape;
|
||||
import net.minecraft.world.level.block.entity.BlockEntity;
|
||||
import net.minecraft.world.level.block.state.BlockState;
|
||||
import net.minecraft.world.level.block.state.StateDefinition;
|
||||
import net.minecraft.world.level.block.state.properties.BlockStateProperties;
|
||||
import net.minecraft.world.level.block.state.properties.BooleanProperty;
|
||||
import net.minecraft.world.level.block.state.properties.DirectionProperty;
|
||||
import net.minecraft.world.phys.BlockHitResult;
|
||||
import net.minecraft.world.phys.shapes.CollisionContext;
|
||||
import net.minecraft.world.phys.shapes.VoxelShape;
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
|
||||
/**
|
||||
* Pet Bed Block - Used by Master to make pets sit and sleep.
|
||||
* Features:
|
||||
* - Right-click cycle: Stand -> SIT (wariza) -> SLEEP (curled) -> Stand
|
||||
* - SIT: immobilized with animation, no night skip
|
||||
* - SLEEP: vanilla sleeping + animation, night skips
|
||||
* - Custom OBJ model rendering
|
||||
*/
|
||||
public class PetBedBlock extends Block implements EntityBlock {
|
||||
|
||||
public static final DirectionProperty FACING =
|
||||
HorizontalDirectionalBlock.FACING;
|
||||
public static final BooleanProperty OCCUPIED =
|
||||
BlockStateProperties.OCCUPIED;
|
||||
|
||||
// Collision shape (rough approximation of pet bed)
|
||||
private static final VoxelShape SHAPE = Block.box(
|
||||
1.0,
|
||||
0.0,
|
||||
1.0,
|
||||
15.0,
|
||||
6.0,
|
||||
15.0
|
||||
);
|
||||
|
||||
public PetBedBlock(Properties properties) {
|
||||
super(properties);
|
||||
this.registerDefaultState(
|
||||
this.stateDefinition.any()
|
||||
.setValue(FACING, Direction.NORTH)
|
||||
.setValue(OCCUPIED, false)
|
||||
);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void createBlockStateDefinition(
|
||||
StateDefinition.Builder<Block, BlockState> builder
|
||||
) {
|
||||
builder.add(FACING, OCCUPIED);
|
||||
}
|
||||
|
||||
@Override
|
||||
public BlockState getStateForPlacement(BlockPlaceContext context) {
|
||||
return this.defaultBlockState().setValue(
|
||||
FACING,
|
||||
context.getHorizontalDirection().getOpposite()
|
||||
);
|
||||
}
|
||||
|
||||
@Override
|
||||
public VoxelShape getShape(
|
||||
BlockState state,
|
||||
BlockGetter level,
|
||||
BlockPos pos,
|
||||
CollisionContext context
|
||||
) {
|
||||
return SHAPE;
|
||||
}
|
||||
|
||||
@Override
|
||||
public RenderShape getRenderShape(BlockState state) {
|
||||
return RenderShape.ENTITYBLOCK_ANIMATED; // Use block entity renderer
|
||||
}
|
||||
|
||||
@Nullable
|
||||
@Override
|
||||
public BlockEntity newBlockEntity(BlockPos pos, BlockState state) {
|
||||
return new PetBedBlockEntity(V2BlockEntities.PET_BED.get(), pos, state);
|
||||
}
|
||||
|
||||
@Override
|
||||
public InteractionResult use(
|
||||
BlockState state,
|
||||
Level level,
|
||||
BlockPos pos,
|
||||
Player player,
|
||||
InteractionHand hand,
|
||||
BlockHitResult hit
|
||||
) {
|
||||
if (level.isClientSide) {
|
||||
return InteractionResult.CONSUME;
|
||||
}
|
||||
|
||||
if (!(player instanceof ServerPlayer sp)) {
|
||||
return InteractionResult.PASS;
|
||||
}
|
||||
|
||||
// SLEEP -> Stand: player is sleeping, wake them up
|
||||
if (sp.isSleeping()) {
|
||||
sp.stopSleeping();
|
||||
PetBedManager.clearPlayer(sp);
|
||||
TiedUpMod.LOGGER.debug(
|
||||
"[PetBedBlock] {} woke up from bed at {}",
|
||||
player.getName().getString(),
|
||||
pos
|
||||
);
|
||||
return InteractionResult.SUCCESS;
|
||||
}
|
||||
|
||||
// SIT -> SLEEP: player is already sitting on this bed
|
||||
if (PetBedManager.isOnBed(sp, pos)) {
|
||||
sp.startSleeping(pos);
|
||||
// FIX: notify server to count this player for night skip
|
||||
if (level instanceof ServerLevel sl) {
|
||||
sl.updateSleepingPlayerList();
|
||||
}
|
||||
PetBedManager.setMode(sp, pos, PetBedManager.PetBedMode.SLEEP);
|
||||
TiedUpMod.LOGGER.debug(
|
||||
"[PetBedBlock] {} started sleeping in bed at {}",
|
||||
player.getName().getString(),
|
||||
pos
|
||||
);
|
||||
return InteractionResult.SUCCESS;
|
||||
}
|
||||
|
||||
// Check if bed is already occupied by someone else
|
||||
BlockEntity be = level.getBlockEntity(pos);
|
||||
if (be instanceof PetBedBlockEntity petBed && petBed.isOccupied()) {
|
||||
return InteractionResult.FAIL;
|
||||
}
|
||||
|
||||
// Stand -> SIT
|
||||
PetBedManager.setMode(sp, pos, PetBedManager.PetBedMode.SIT);
|
||||
TiedUpMod.LOGGER.debug(
|
||||
"[PetBedBlock] {} sat down on bed at {}",
|
||||
player.getName().getString(),
|
||||
pos
|
||||
);
|
||||
return InteractionResult.SUCCESS;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setBedOccupied(
|
||||
BlockState state,
|
||||
Level level,
|
||||
BlockPos pos,
|
||||
net.minecraft.world.entity.LivingEntity sleeper,
|
||||
boolean occupied
|
||||
) {
|
||||
level.setBlock(pos, state.setValue(OCCUPIED, occupied), 3);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isBed(
|
||||
BlockState state,
|
||||
BlockGetter level,
|
||||
BlockPos pos,
|
||||
@Nullable net.minecraft.world.entity.Entity player
|
||||
) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
148
src/main/java/com/tiedup/remake/v2/blocks/PetBedBlockEntity.java
Normal file
148
src/main/java/com/tiedup/remake/v2/blocks/PetBedBlockEntity.java
Normal file
@@ -0,0 +1,148 @@
|
||||
package com.tiedup.remake.v2.blocks;
|
||||
|
||||
import com.tiedup.remake.core.TiedUpMod;
|
||||
import java.util.UUID;
|
||||
import javax.annotation.Nullable;
|
||||
import net.minecraft.core.BlockPos;
|
||||
import net.minecraft.nbt.CompoundTag;
|
||||
import net.minecraft.resources.ResourceLocation;
|
||||
import net.minecraft.world.level.block.entity.BlockEntityType;
|
||||
import net.minecraft.world.level.block.state.BlockState;
|
||||
|
||||
/**
|
||||
* Block entity for Pet Bed.
|
||||
* Used by Master to make pets sleep. Players must crouch to use it.
|
||||
*/
|
||||
public class PetBedBlockEntity extends ObjBlockEntity {
|
||||
|
||||
public static final ResourceLocation MODEL_LOCATION =
|
||||
ResourceLocation.fromNamespaceAndPath(
|
||||
TiedUpMod.MOD_ID,
|
||||
"models/obj/blocks/bed/model.obj"
|
||||
);
|
||||
|
||||
public static final ResourceLocation TEXTURE_LOCATION =
|
||||
ResourceLocation.fromNamespaceAndPath(
|
||||
TiedUpMod.MOD_ID,
|
||||
"textures/block/pet_bed.png"
|
||||
);
|
||||
|
||||
/** UUID of the pet currently using this bed (null if empty) */
|
||||
@Nullable
|
||||
private UUID occupantUUID = null;
|
||||
|
||||
/** UUID of the Master who owns this bed (null = public) */
|
||||
@Nullable
|
||||
private UUID ownerUUID = null;
|
||||
|
||||
public PetBedBlockEntity(
|
||||
BlockEntityType<?> type,
|
||||
BlockPos pos,
|
||||
BlockState state
|
||||
) {
|
||||
super(type, pos, state);
|
||||
}
|
||||
|
||||
@Override
|
||||
public ResourceLocation getModelLocation() {
|
||||
return MODEL_LOCATION;
|
||||
}
|
||||
|
||||
@Override
|
||||
public ResourceLocation getTextureLocation() {
|
||||
return TEXTURE_LOCATION;
|
||||
}
|
||||
|
||||
@Override
|
||||
public float getModelScale() {
|
||||
return 0.8f; // Scale down to fit in block
|
||||
}
|
||||
|
||||
@Override
|
||||
public float[] getModelOffset() {
|
||||
return new float[] { 0.0f, 0.0f, 0.0f };
|
||||
}
|
||||
|
||||
// ========================================
|
||||
// OCCUPANCY
|
||||
// ========================================
|
||||
|
||||
public boolean isOccupied() {
|
||||
return occupantUUID != null;
|
||||
}
|
||||
|
||||
@Nullable
|
||||
public UUID getOccupantUUID() {
|
||||
return occupantUUID;
|
||||
}
|
||||
|
||||
/**
|
||||
* Have a pet occupy this bed.
|
||||
*
|
||||
* @param petUUID The pet's UUID
|
||||
* @return true if successful, false if already occupied
|
||||
*/
|
||||
public boolean occupy(UUID petUUID) {
|
||||
if (isOccupied()) return false;
|
||||
|
||||
this.occupantUUID = petUUID;
|
||||
setChanged();
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Release the current occupant.
|
||||
*/
|
||||
public void release() {
|
||||
this.occupantUUID = null;
|
||||
setChanged();
|
||||
}
|
||||
|
||||
// ========================================
|
||||
// OWNERSHIP
|
||||
// ========================================
|
||||
|
||||
@Nullable
|
||||
public UUID getOwnerUUID() {
|
||||
return ownerUUID;
|
||||
}
|
||||
|
||||
public void setOwner(@Nullable UUID masterUUID) {
|
||||
this.ownerUUID = masterUUID;
|
||||
setChanged();
|
||||
}
|
||||
|
||||
public boolean isOwnedBy(UUID uuid) {
|
||||
return ownerUUID != null && ownerUUID.equals(uuid);
|
||||
}
|
||||
|
||||
public boolean isPublic() {
|
||||
return ownerUUID == null;
|
||||
}
|
||||
|
||||
// ========================================
|
||||
// NBT
|
||||
// ========================================
|
||||
|
||||
@Override
|
||||
protected void saveAdditional(CompoundTag tag) {
|
||||
super.saveAdditional(tag);
|
||||
if (occupantUUID != null) {
|
||||
tag.putUUID("occupant", occupantUUID);
|
||||
}
|
||||
if (ownerUUID != null) {
|
||||
tag.putUUID("owner", ownerUUID);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void load(CompoundTag tag) {
|
||||
super.load(tag);
|
||||
if (tag.hasUUID("occupant")) {
|
||||
this.occupantUUID = tag.getUUID("occupant");
|
||||
}
|
||||
if (tag.hasUUID("owner")) {
|
||||
this.ownerUUID = tag.getUUID("owner");
|
||||
}
|
||||
}
|
||||
}
|
||||
278
src/main/java/com/tiedup/remake/v2/blocks/PetBedManager.java
Normal file
278
src/main/java/com/tiedup/remake/v2/blocks/PetBedManager.java
Normal file
@@ -0,0 +1,278 @@
|
||||
package com.tiedup.remake.v2.blocks;
|
||||
|
||||
import com.tiedup.remake.core.TiedUpMod;
|
||||
import com.tiedup.remake.network.ModNetwork;
|
||||
import com.tiedup.remake.network.sync.PacketSyncPetBedState;
|
||||
import java.util.Map;
|
||||
import java.util.UUID;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
import net.minecraft.core.BlockPos;
|
||||
import net.minecraft.server.level.ServerLevel;
|
||||
import net.minecraft.server.level.ServerPlayer;
|
||||
import net.minecraft.world.entity.ai.attributes.AttributeInstance;
|
||||
import net.minecraft.world.entity.ai.attributes.AttributeModifier;
|
||||
import net.minecraft.world.entity.ai.attributes.Attributes;
|
||||
import net.minecraft.world.level.Level;
|
||||
import net.minecraft.world.level.block.entity.BlockEntity;
|
||||
import net.minecraft.world.level.block.state.BlockState;
|
||||
|
||||
/**
|
||||
* Server-side tracker for pet bed sit/sleep state per player.
|
||||
* Manages immobilization, block entity occupancy, and client sync.
|
||||
*/
|
||||
public class PetBedManager {
|
||||
|
||||
public enum PetBedMode {
|
||||
SIT,
|
||||
SLEEP,
|
||||
}
|
||||
|
||||
private static final UUID PET_BED_SPEED_MODIFIER_UUID = UUID.fromString(
|
||||
"a1b2c3d4-e5f6-4789-abcd-ef0123456789"
|
||||
);
|
||||
private static final String PET_BED_SPEED_MODIFIER_NAME =
|
||||
"tiedup.pet_bed_immobilize";
|
||||
private static final double FULL_IMMOBILIZATION = -0.10;
|
||||
|
||||
/** Active pet bed entries keyed by player UUID */
|
||||
private static final Map<UUID, PetBedEntry> entries =
|
||||
new ConcurrentHashMap<>();
|
||||
|
||||
private static class PetBedEntry {
|
||||
|
||||
final BlockPos pos;
|
||||
PetBedMode mode;
|
||||
|
||||
PetBedEntry(BlockPos pos, PetBedMode mode) {
|
||||
this.pos = pos;
|
||||
this.mode = mode;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Set a player's pet bed mode (SIT or SLEEP).
|
||||
*/
|
||||
public static void setMode(
|
||||
ServerPlayer player,
|
||||
BlockPos pos,
|
||||
PetBedMode mode
|
||||
) {
|
||||
UUID uuid = player.getUUID();
|
||||
PetBedEntry existing = entries.get(uuid);
|
||||
|
||||
if (existing != null && mode == PetBedMode.SLEEP) {
|
||||
// Transitioning from SIT to SLEEP — update mode
|
||||
existing.mode = mode;
|
||||
} else {
|
||||
// New entry (Stand → SIT)
|
||||
entries.put(uuid, new PetBedEntry(pos.immutable(), mode));
|
||||
}
|
||||
|
||||
// Occupy the block entity
|
||||
BlockEntity be = player.level().getBlockEntity(pos);
|
||||
if (be instanceof PetBedBlockEntity petBed) {
|
||||
petBed.occupy(uuid);
|
||||
}
|
||||
|
||||
// Teleport player to bed center and set rotation to bed facing
|
||||
double cx = pos.getX() + 0.5;
|
||||
double cy = pos.getY();
|
||||
double cz = pos.getZ() + 0.5;
|
||||
|
||||
// Nudge player slightly backward on the bed
|
||||
float bedYRot = getBedFacingAngle(player.level(), pos);
|
||||
double rad = Math.toRadians(bedYRot);
|
||||
double backOffset = 0.15;
|
||||
cx += Math.sin(rad) * backOffset;
|
||||
cz -= Math.cos(rad) * backOffset;
|
||||
|
||||
player.teleportTo(cx, cy, cz);
|
||||
|
||||
player.setYRot(bedYRot);
|
||||
player.setYBodyRot(bedYRot);
|
||||
player.setYHeadRot(bedYRot);
|
||||
|
||||
// Immobilize the player (both SIT and SLEEP)
|
||||
applySpeedModifier(player);
|
||||
|
||||
// Sync to all clients
|
||||
PacketSyncPetBedState packet = new PacketSyncPetBedState(
|
||||
uuid,
|
||||
mode == PetBedMode.SIT ? (byte) 1 : (byte) 2,
|
||||
pos
|
||||
);
|
||||
ModNetwork.sendToAllTrackingAndSelf(packet, player);
|
||||
|
||||
TiedUpMod.LOGGER.debug(
|
||||
"[PetBedManager] {} -> {} at {}",
|
||||
player.getName().getString(),
|
||||
mode,
|
||||
pos
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Clear a player from the pet bed system.
|
||||
*/
|
||||
public static void clearPlayer(ServerPlayer player) {
|
||||
UUID uuid = player.getUUID();
|
||||
PetBedEntry entry = entries.remove(uuid);
|
||||
if (entry == null) return;
|
||||
|
||||
// Release block entity
|
||||
BlockEntity be = player.level().getBlockEntity(entry.pos);
|
||||
if (be instanceof PetBedBlockEntity petBed) {
|
||||
petBed.release();
|
||||
}
|
||||
|
||||
// Restore speed
|
||||
removeSpeedModifier(player);
|
||||
|
||||
// Sync clear to clients
|
||||
PacketSyncPetBedState packet = new PacketSyncPetBedState(
|
||||
uuid,
|
||||
(byte) 0,
|
||||
entry.pos
|
||||
);
|
||||
ModNetwork.sendToAllTrackingAndSelf(packet, player);
|
||||
|
||||
TiedUpMod.LOGGER.debug(
|
||||
"[PetBedManager] {} cleared from pet bed at {}",
|
||||
player.getName().getString(),
|
||||
entry.pos
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if a player is on a pet bed at the given position.
|
||||
*/
|
||||
public static boolean isOnBed(ServerPlayer player, BlockPos pos) {
|
||||
PetBedEntry entry = entries.get(player.getUUID());
|
||||
return entry != null && entry.pos.equals(pos);
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if a player is on any pet bed.
|
||||
*/
|
||||
public static boolean isOnAnyBed(ServerPlayer player) {
|
||||
return entries.containsKey(player.getUUID());
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the current mode for a player, or null if not on a bed.
|
||||
*/
|
||||
@Nullable
|
||||
public static PetBedMode getMode(ServerPlayer player) {
|
||||
PetBedEntry entry = entries.get(player.getUUID());
|
||||
return entry != null ? entry.mode : null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Tick a player to check if pet bed state should be cancelled.
|
||||
* Called from server-side tick handler.
|
||||
*/
|
||||
public static void tickPlayer(ServerPlayer player) {
|
||||
PetBedEntry entry = entries.get(player.getUUID());
|
||||
if (entry == null) return;
|
||||
|
||||
// SLEEP: detect vanilla wakeup (night skip, etc.)
|
||||
if (entry.mode == PetBedMode.SLEEP && !player.isSleeping()) {
|
||||
clearPlayer(player);
|
||||
return;
|
||||
}
|
||||
|
||||
// Both modes: lock body rotation to bed facing (prevents camera from rotating model)
|
||||
float bedYRot = getBedFacingAngle(player.level(), entry.pos);
|
||||
player.setYBodyRot(bedYRot);
|
||||
|
||||
// SLEEP: enforce correct Y position (vanilla startSleeping sets Y+0.2)
|
||||
if (entry.mode == PetBedMode.SLEEP) {
|
||||
double correctY = entry.pos.getY();
|
||||
if (Math.abs(player.getY() - correctY) > 0.01) {
|
||||
player.teleportTo(player.getX(), correctY, player.getZ());
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// SIT: cancel on sneak (like dismounting a vehicle)
|
||||
if (entry.mode != PetBedMode.SIT) return;
|
||||
|
||||
if (player.isShiftKeyDown()) {
|
||||
clearPlayer(player);
|
||||
return;
|
||||
}
|
||||
|
||||
// Check if player moved too far from bed
|
||||
double distSq = player.blockPosition().distSqr(entry.pos);
|
||||
if (distSq > 2.25) {
|
||||
// > 1.5 blocks
|
||||
clearPlayer(player);
|
||||
return;
|
||||
}
|
||||
|
||||
// Check if the block is still a pet bed
|
||||
if (
|
||||
!(player.level().getBlockState(entry.pos).getBlock() instanceof
|
||||
PetBedBlock)
|
||||
) {
|
||||
clearPlayer(player);
|
||||
}
|
||||
}
|
||||
|
||||
private static float getBedFacingAngle(Level level, BlockPos pos) {
|
||||
BlockState state = level.getBlockState(pos);
|
||||
if (state.hasProperty(PetBedBlock.FACING)) {
|
||||
return state.getValue(PetBedBlock.FACING).toYRot();
|
||||
}
|
||||
return 0f;
|
||||
}
|
||||
|
||||
/**
|
||||
* Clean up when a player disconnects.
|
||||
*/
|
||||
public static void onPlayerDisconnect(UUID uuid) {
|
||||
entries.remove(uuid);
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove any leftover pet bed speed modifier on login.
|
||||
* The modifier persists on the entity through save/load, but the entries map doesn't,
|
||||
* so we clean it up here to prevent the player from being stuck.
|
||||
*/
|
||||
public static void onPlayerLogin(ServerPlayer player) {
|
||||
removeSpeedModifier(player);
|
||||
}
|
||||
|
||||
private static void applySpeedModifier(ServerPlayer player) {
|
||||
AttributeInstance movementSpeed = player.getAttribute(
|
||||
Attributes.MOVEMENT_SPEED
|
||||
);
|
||||
if (movementSpeed == null) return;
|
||||
|
||||
// Remove existing to avoid duplicates
|
||||
if (movementSpeed.getModifier(PET_BED_SPEED_MODIFIER_UUID) != null) {
|
||||
movementSpeed.removeModifier(PET_BED_SPEED_MODIFIER_UUID);
|
||||
}
|
||||
|
||||
AttributeModifier modifier = new AttributeModifier(
|
||||
PET_BED_SPEED_MODIFIER_UUID,
|
||||
PET_BED_SPEED_MODIFIER_NAME,
|
||||
FULL_IMMOBILIZATION,
|
||||
AttributeModifier.Operation.ADDITION
|
||||
);
|
||||
movementSpeed.addPermanentModifier(modifier);
|
||||
}
|
||||
|
||||
private static void removeSpeedModifier(ServerPlayer player) {
|
||||
AttributeInstance movementSpeed = player.getAttribute(
|
||||
Attributes.MOVEMENT_SPEED
|
||||
);
|
||||
if (
|
||||
movementSpeed != null &&
|
||||
movementSpeed.getModifier(PET_BED_SPEED_MODIFIER_UUID) != null
|
||||
) {
|
||||
movementSpeed.removeModifier(PET_BED_SPEED_MODIFIER_UUID);
|
||||
}
|
||||
}
|
||||
}
|
||||
174
src/main/java/com/tiedup/remake/v2/blocks/PetBowlBlock.java
Normal file
174
src/main/java/com/tiedup/remake/v2/blocks/PetBowlBlock.java
Normal file
@@ -0,0 +1,174 @@
|
||||
package com.tiedup.remake.v2.blocks;
|
||||
|
||||
import com.tiedup.remake.core.TiedUpMod;
|
||||
import com.tiedup.remake.v2.V2BlockEntities;
|
||||
import net.minecraft.core.BlockPos;
|
||||
import net.minecraft.core.Direction;
|
||||
import net.minecraft.server.level.ServerPlayer;
|
||||
import net.minecraft.world.InteractionHand;
|
||||
import net.minecraft.world.InteractionResult;
|
||||
import net.minecraft.world.entity.player.Player;
|
||||
import net.minecraft.world.item.ItemStack;
|
||||
import net.minecraft.world.item.Items;
|
||||
import net.minecraft.world.item.context.BlockPlaceContext;
|
||||
import net.minecraft.world.level.BlockGetter;
|
||||
import net.minecraft.world.level.Level;
|
||||
import net.minecraft.world.level.block.Block;
|
||||
import net.minecraft.world.level.block.EntityBlock;
|
||||
import net.minecraft.world.level.block.HorizontalDirectionalBlock;
|
||||
import net.minecraft.world.level.block.RenderShape;
|
||||
import net.minecraft.world.level.block.entity.BlockEntity;
|
||||
import net.minecraft.world.level.block.state.BlockState;
|
||||
import net.minecraft.world.level.block.state.StateDefinition;
|
||||
import net.minecraft.world.level.block.state.properties.DirectionProperty;
|
||||
import net.minecraft.world.phys.BlockHitResult;
|
||||
import net.minecraft.world.phys.shapes.CollisionContext;
|
||||
import net.minecraft.world.phys.shapes.VoxelShape;
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
|
||||
/**
|
||||
* Pet Bowl Block - Used by Master to feed pets.
|
||||
* Features:
|
||||
* - Right-click with food to fill
|
||||
* - Crouch + right-click to eat (only for pets)
|
||||
* - Custom OBJ model rendering
|
||||
*/
|
||||
public class PetBowlBlock extends Block implements EntityBlock {
|
||||
|
||||
public static final DirectionProperty FACING =
|
||||
HorizontalDirectionalBlock.FACING;
|
||||
|
||||
// Collision shape (rough approximation of bowl)
|
||||
private static final VoxelShape SHAPE = Block.box(
|
||||
3.0,
|
||||
0.0,
|
||||
3.0,
|
||||
13.0,
|
||||
4.0,
|
||||
13.0
|
||||
);
|
||||
|
||||
public PetBowlBlock(Properties properties) {
|
||||
super(properties);
|
||||
this.registerDefaultState(
|
||||
this.stateDefinition.any().setValue(FACING, Direction.NORTH)
|
||||
);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void createBlockStateDefinition(
|
||||
StateDefinition.Builder<Block, BlockState> builder
|
||||
) {
|
||||
builder.add(FACING);
|
||||
}
|
||||
|
||||
@Override
|
||||
public BlockState getStateForPlacement(BlockPlaceContext context) {
|
||||
return this.defaultBlockState().setValue(
|
||||
FACING,
|
||||
context.getHorizontalDirection().getOpposite()
|
||||
);
|
||||
}
|
||||
|
||||
@Override
|
||||
public VoxelShape getShape(
|
||||
BlockState state,
|
||||
BlockGetter level,
|
||||
BlockPos pos,
|
||||
CollisionContext context
|
||||
) {
|
||||
return SHAPE;
|
||||
}
|
||||
|
||||
@Override
|
||||
public RenderShape getRenderShape(BlockState state) {
|
||||
return RenderShape.ENTITYBLOCK_ANIMATED; // Use block entity renderer
|
||||
}
|
||||
|
||||
@Nullable
|
||||
@Override
|
||||
public BlockEntity newBlockEntity(BlockPos pos, BlockState state) {
|
||||
return new PetBowlBlockEntity(
|
||||
V2BlockEntities.PET_BOWL.get(),
|
||||
pos,
|
||||
state
|
||||
);
|
||||
}
|
||||
|
||||
@Override
|
||||
public InteractionResult use(
|
||||
BlockState state,
|
||||
Level level,
|
||||
BlockPos pos,
|
||||
Player player,
|
||||
InteractionHand hand,
|
||||
BlockHitResult hit
|
||||
) {
|
||||
if (level.isClientSide) {
|
||||
return InteractionResult.SUCCESS;
|
||||
}
|
||||
|
||||
BlockEntity be = level.getBlockEntity(pos);
|
||||
if (!(be instanceof PetBowlBlockEntity bowl)) {
|
||||
return InteractionResult.PASS;
|
||||
}
|
||||
|
||||
ItemStack heldItem = player.getItemInHand(hand);
|
||||
|
||||
// Fill bowl with food items
|
||||
if (isFood(heldItem)) {
|
||||
int foodValue = getFoodValue(heldItem);
|
||||
bowl.fillBowl(foodValue);
|
||||
|
||||
if (!player.getAbilities().instabuild) {
|
||||
heldItem.shrink(1);
|
||||
}
|
||||
|
||||
TiedUpMod.LOGGER.debug(
|
||||
"[PetBowlBlock] {} filled bowl at {} with {}",
|
||||
player.getName().getString(),
|
||||
pos,
|
||||
heldItem.getItem()
|
||||
);
|
||||
|
||||
return InteractionResult.CONSUME;
|
||||
}
|
||||
|
||||
// Eat from bowl (empty hand)
|
||||
if (heldItem.isEmpty() && bowl.hasFood()) {
|
||||
// TODO: Check if player is a pet and allowed to eat
|
||||
int consumed = bowl.eatFromBowl();
|
||||
if (consumed > 0) {
|
||||
player.getFoodData().eat(consumed, 0.6f);
|
||||
|
||||
TiedUpMod.LOGGER.debug(
|
||||
"[PetBowlBlock] {} ate {} from bowl at {}",
|
||||
player.getName().getString(),
|
||||
consumed,
|
||||
pos
|
||||
);
|
||||
}
|
||||
return InteractionResult.CONSUME;
|
||||
}
|
||||
|
||||
return InteractionResult.PASS;
|
||||
}
|
||||
|
||||
private boolean isFood(ItemStack stack) {
|
||||
if (stack.isEmpty()) return false;
|
||||
return (
|
||||
stack.getItem().isEdible() ||
|
||||
stack.is(Items.WHEAT) ||
|
||||
stack.is(Items.CARROT) ||
|
||||
stack.is(Items.APPLE)
|
||||
);
|
||||
}
|
||||
|
||||
private int getFoodValue(ItemStack stack) {
|
||||
if (stack.getItem().isEdible()) {
|
||||
var food = stack.getItem().getFoodProperties();
|
||||
return food != null ? food.getNutrition() : 2;
|
||||
}
|
||||
return 2; // Default for non-standard foods
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,122 @@
|
||||
package com.tiedup.remake.v2.blocks;
|
||||
|
||||
import com.tiedup.remake.core.TiedUpMod;
|
||||
import net.minecraft.core.BlockPos;
|
||||
import net.minecraft.nbt.CompoundTag;
|
||||
import net.minecraft.resources.ResourceLocation;
|
||||
import net.minecraft.world.level.block.entity.BlockEntityType;
|
||||
import net.minecraft.world.level.block.state.BlockState;
|
||||
|
||||
/**
|
||||
* Block entity for Pet Bowl.
|
||||
* Used by Master to feed pets. Players must crouch to eat from it.
|
||||
*/
|
||||
public class PetBowlBlockEntity extends ObjBlockEntity {
|
||||
|
||||
public static final ResourceLocation MODEL_LOCATION =
|
||||
ResourceLocation.fromNamespaceAndPath(
|
||||
TiedUpMod.MOD_ID,
|
||||
"models/obj/blocks/bowl/model.obj"
|
||||
);
|
||||
|
||||
public static final ResourceLocation TEXTURE_LOCATION =
|
||||
ResourceLocation.fromNamespaceAndPath(
|
||||
TiedUpMod.MOD_ID,
|
||||
"textures/block/pet_bowl.png"
|
||||
);
|
||||
|
||||
/** Whether the bowl currently has food */
|
||||
private boolean hasFood = false;
|
||||
|
||||
/** Food saturation level (0-20) */
|
||||
private int foodLevel = 0;
|
||||
|
||||
public PetBowlBlockEntity(
|
||||
BlockEntityType<?> type,
|
||||
BlockPos pos,
|
||||
BlockState state
|
||||
) {
|
||||
super(type, pos, state);
|
||||
}
|
||||
|
||||
@Override
|
||||
public ResourceLocation getModelLocation() {
|
||||
return MODEL_LOCATION;
|
||||
}
|
||||
|
||||
@Override
|
||||
public ResourceLocation getTextureLocation() {
|
||||
return TEXTURE_LOCATION;
|
||||
}
|
||||
|
||||
@Override
|
||||
public float getModelScale() {
|
||||
return 1.0f; // Adjust based on model size
|
||||
}
|
||||
|
||||
// ========================================
|
||||
// FOOD MANAGEMENT
|
||||
// ========================================
|
||||
|
||||
public boolean hasFood() {
|
||||
return hasFood && foodLevel > 0;
|
||||
}
|
||||
|
||||
public int getFoodLevel() {
|
||||
return foodLevel;
|
||||
}
|
||||
|
||||
/**
|
||||
* Fill the bowl with food.
|
||||
*
|
||||
* @param amount Amount of food to add (saturation points)
|
||||
*/
|
||||
public void fillBowl(int amount) {
|
||||
this.foodLevel = Math.min(20, this.foodLevel + amount);
|
||||
this.hasFood = this.foodLevel > 0;
|
||||
setChanged();
|
||||
}
|
||||
|
||||
/**
|
||||
* Pet eats from the bowl.
|
||||
*
|
||||
* @return Amount of food consumed (0 if empty)
|
||||
*/
|
||||
public int eatFromBowl() {
|
||||
if (!hasFood()) return 0;
|
||||
|
||||
int consumed = Math.min(4, foodLevel); // Eat up to 4 at a time
|
||||
foodLevel -= consumed;
|
||||
hasFood = foodLevel > 0;
|
||||
setChanged();
|
||||
|
||||
return consumed;
|
||||
}
|
||||
|
||||
/**
|
||||
* Empty the bowl completely.
|
||||
*/
|
||||
public void emptyBowl() {
|
||||
this.foodLevel = 0;
|
||||
this.hasFood = false;
|
||||
setChanged();
|
||||
}
|
||||
|
||||
// ========================================
|
||||
// NBT
|
||||
// ========================================
|
||||
|
||||
@Override
|
||||
protected void saveAdditional(CompoundTag tag) {
|
||||
super.saveAdditional(tag);
|
||||
tag.putBoolean("hasFood", hasFood);
|
||||
tag.putInt("foodLevel", foodLevel);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void load(CompoundTag tag) {
|
||||
super.load(tag);
|
||||
this.hasFood = tag.getBoolean("hasFood");
|
||||
this.foodLevel = tag.getInt("foodLevel");
|
||||
}
|
||||
}
|
||||
538
src/main/java/com/tiedup/remake/v2/blocks/PetCageBlock.java
Normal file
538
src/main/java/com/tiedup/remake/v2/blocks/PetCageBlock.java
Normal file
@@ -0,0 +1,538 @@
|
||||
package com.tiedup.remake.v2.blocks;
|
||||
|
||||
import com.tiedup.remake.core.TiedUpMod;
|
||||
import com.tiedup.remake.state.IBondageState;
|
||||
import com.tiedup.remake.util.KidnappedHelper;
|
||||
import com.tiedup.remake.v2.V2BlockEntities;
|
||||
import com.tiedup.remake.v2.V2Blocks;
|
||||
import net.minecraft.core.BlockPos;
|
||||
import net.minecraft.core.Direction;
|
||||
import net.minecraft.server.level.ServerPlayer;
|
||||
import net.minecraft.world.InteractionHand;
|
||||
import net.minecraft.world.InteractionResult;
|
||||
import net.minecraft.world.entity.player.Player;
|
||||
import net.minecraft.world.item.context.BlockPlaceContext;
|
||||
import net.minecraft.world.level.BlockGetter;
|
||||
import net.minecraft.world.level.Level;
|
||||
import net.minecraft.world.level.block.Block;
|
||||
import net.minecraft.world.level.block.Blocks;
|
||||
import net.minecraft.world.level.block.EntityBlock;
|
||||
import net.minecraft.world.level.block.HorizontalDirectionalBlock;
|
||||
import net.minecraft.world.level.block.RenderShape;
|
||||
import net.minecraft.world.level.block.entity.BlockEntity;
|
||||
import net.minecraft.world.level.block.state.BlockState;
|
||||
import net.minecraft.world.level.block.state.StateDefinition;
|
||||
import net.minecraft.world.level.block.state.properties.DirectionProperty;
|
||||
import net.minecraft.world.phys.BlockHitResult;
|
||||
import net.minecraft.world.phys.shapes.CollisionContext;
|
||||
import net.minecraft.world.phys.shapes.Shapes;
|
||||
import net.minecraft.world.phys.shapes.VoxelShape;
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
|
||||
/**
|
||||
* Pet Cage Block (Master) - Multi-block cage (3x3x2).
|
||||
*
|
||||
* <p>Collision is computed from the actual OBJ model dimensions. Each block in
|
||||
* the 3x3x2 grid gets collision shapes for any cage walls that pass through it,
|
||||
* clipped to the block boundaries. This gives pixel-accurate cage collision
|
||||
* that matches the visual model.
|
||||
*/
|
||||
public class PetCageBlock extends Block implements EntityBlock {
|
||||
|
||||
public static final DirectionProperty FACING =
|
||||
HorizontalDirectionalBlock.FACING;
|
||||
|
||||
// ========================================
|
||||
// OBJ MODEL DIMENSIONS (in model space, before rotation/translation)
|
||||
// ========================================
|
||||
private static final double MODEL_MIN_X = -1.137;
|
||||
private static final double MODEL_MAX_X = 1.0;
|
||||
private static final double MODEL_MIN_Z = -1.122;
|
||||
private static final double MODEL_MAX_Z = 1.0;
|
||||
private static final double MODEL_MAX_Y = 2.218;
|
||||
|
||||
/** Wall thickness in blocks. */
|
||||
private static final double WALL_T = 0.125; // 2 pixels
|
||||
|
||||
private static final VoxelShape OUTLINE_SHAPE = Block.box(
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
16,
|
||||
16,
|
||||
16
|
||||
);
|
||||
|
||||
public PetCageBlock(Properties properties) {
|
||||
super(properties);
|
||||
this.registerDefaultState(
|
||||
this.stateDefinition.any().setValue(FACING, Direction.NORTH)
|
||||
);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void createBlockStateDefinition(
|
||||
StateDefinition.Builder<Block, BlockState> builder
|
||||
) {
|
||||
builder.add(FACING);
|
||||
}
|
||||
|
||||
@Override
|
||||
public BlockState getStateForPlacement(BlockPlaceContext context) {
|
||||
Direction facing = context.getHorizontalDirection().getOpposite();
|
||||
BlockPos pos = context.getClickedPos();
|
||||
|
||||
for (BlockPos partPos : getPartPositions(pos, facing)) {
|
||||
if (
|
||||
!context
|
||||
.getLevel()
|
||||
.getBlockState(partPos)
|
||||
.canBeReplaced(context)
|
||||
) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
return this.defaultBlockState().setValue(FACING, facing);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onPlace(
|
||||
BlockState state,
|
||||
Level level,
|
||||
BlockPos pos,
|
||||
BlockState oldState,
|
||||
boolean isMoving
|
||||
) {
|
||||
super.onPlace(state, level, pos, oldState, isMoving);
|
||||
if (level.isClientSide || oldState.is(this)) return;
|
||||
|
||||
Direction facing = state.getValue(FACING);
|
||||
BlockState partState = V2Blocks.PET_CAGE_PART.get()
|
||||
.defaultBlockState()
|
||||
.setValue(PetCagePartBlock.FACING, facing);
|
||||
|
||||
for (BlockPos partPos : getPartPositions(pos, facing)) {
|
||||
level.setBlock(partPos, partState, 3);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public VoxelShape getShape(
|
||||
BlockState state,
|
||||
BlockGetter level,
|
||||
BlockPos pos,
|
||||
CollisionContext context
|
||||
) {
|
||||
return OUTLINE_SHAPE;
|
||||
}
|
||||
|
||||
@Override
|
||||
public VoxelShape getCollisionShape(
|
||||
BlockState state,
|
||||
BlockGetter level,
|
||||
BlockPos pos,
|
||||
CollisionContext context
|
||||
) {
|
||||
Direction facing = state.getValue(FACING);
|
||||
return computeCageCollision(pos, facing, pos);
|
||||
}
|
||||
|
||||
@Override
|
||||
public RenderShape getRenderShape(BlockState state) {
|
||||
return RenderShape.ENTITYBLOCK_ANIMATED;
|
||||
}
|
||||
|
||||
@Nullable
|
||||
@Override
|
||||
public BlockEntity newBlockEntity(BlockPos pos, BlockState state) {
|
||||
return new PetCageBlockEntity(
|
||||
V2BlockEntities.PET_CAGE.get(),
|
||||
pos,
|
||||
state
|
||||
);
|
||||
}
|
||||
|
||||
@Override
|
||||
public InteractionResult use(
|
||||
BlockState state,
|
||||
Level level,
|
||||
BlockPos pos,
|
||||
Player player,
|
||||
InteractionHand hand,
|
||||
BlockHitResult hit
|
||||
) {
|
||||
if (level.isClientSide) {
|
||||
return InteractionResult.CONSUME;
|
||||
}
|
||||
|
||||
if (!(player instanceof ServerPlayer sp)) {
|
||||
return InteractionResult.PASS;
|
||||
}
|
||||
|
||||
BlockEntity be = level.getBlockEntity(pos);
|
||||
if (!(be instanceof PetCageBlockEntity cageBE)) {
|
||||
return InteractionResult.PASS;
|
||||
}
|
||||
|
||||
if (cageBE.hasOccupant()) {
|
||||
ServerPlayer occupant = cageBE.getOccupant(level.getServer());
|
||||
if (occupant != null) {
|
||||
PetCageManager.releasePlayer(occupant);
|
||||
TiedUpMod.LOGGER.debug(
|
||||
"[PetCageBlock] {} released {} from cage at {}",
|
||||
player.getName().getString(),
|
||||
occupant.getName().getString(),
|
||||
pos
|
||||
);
|
||||
} else {
|
||||
cageBE.clearOccupant();
|
||||
}
|
||||
return InteractionResult.SUCCESS;
|
||||
}
|
||||
|
||||
double cx = pos.getX() + 0.5;
|
||||
double cy = pos.getY();
|
||||
double cz = pos.getZ() + 0.5;
|
||||
|
||||
for (ServerPlayer nearby : level
|
||||
.getServer()
|
||||
.getPlayerList()
|
||||
.getPlayers()) {
|
||||
if (nearby == player) continue;
|
||||
if (nearby.distanceToSqr(cx, cy, cz) > 9.0) continue;
|
||||
|
||||
IBondageState kidState = KidnappedHelper.getKidnappedState(nearby);
|
||||
if (kidState != null && kidState.isTiedUp()) {
|
||||
PetCageManager.cagePlayer(nearby, pos);
|
||||
cageBE.setOccupant(nearby.getUUID());
|
||||
TiedUpMod.LOGGER.debug(
|
||||
"[PetCageBlock] {} caged {} at {}",
|
||||
player.getName().getString(),
|
||||
nearby.getName().getString(),
|
||||
pos
|
||||
);
|
||||
return InteractionResult.SUCCESS;
|
||||
}
|
||||
}
|
||||
|
||||
return InteractionResult.PASS;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onRemove(
|
||||
BlockState state,
|
||||
Level level,
|
||||
BlockPos pos,
|
||||
BlockState newState,
|
||||
boolean isMoving
|
||||
) {
|
||||
if (!state.is(newState.getBlock())) {
|
||||
BlockEntity be = level.getBlockEntity(pos);
|
||||
if (
|
||||
be instanceof PetCageBlockEntity cageBE &&
|
||||
cageBE.hasOccupant() &&
|
||||
level.getServer() != null
|
||||
) {
|
||||
ServerPlayer occupant = cageBE.getOccupant(level.getServer());
|
||||
if (occupant != null) {
|
||||
PetCageManager.releasePlayer(occupant);
|
||||
}
|
||||
}
|
||||
|
||||
Direction facing = state.getValue(FACING);
|
||||
for (BlockPos partPos : getPartPositions(pos, facing)) {
|
||||
BlockState partState = level.getBlockState(partPos);
|
||||
if (partState.getBlock() instanceof PetCagePartBlock) {
|
||||
level.setBlock(
|
||||
partPos,
|
||||
Blocks.AIR.defaultBlockState(),
|
||||
3 | 64
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
super.onRemove(state, level, pos, newState, isMoving);
|
||||
}
|
||||
|
||||
// ========================================
|
||||
// CAGE COLLISION (model-accurate)
|
||||
// ========================================
|
||||
|
||||
/**
|
||||
* Compute the cage AABB in world coordinates for a given master position and facing.
|
||||
* The renderer does translate(0.5, 0, 0.5) then rotate, so vertices are
|
||||
* first rotated then translated.
|
||||
*
|
||||
* @return {minX, minZ, maxX, maxZ, maxY}
|
||||
*/
|
||||
public static double[] getCageWorldBounds(
|
||||
BlockPos masterPos,
|
||||
Direction facing
|
||||
) {
|
||||
double cx = masterPos.getX() + 0.5;
|
||||
double cz = masterPos.getZ() + 0.5;
|
||||
|
||||
double wMinX, wMaxX, wMinZ, wMaxZ;
|
||||
|
||||
switch (facing) {
|
||||
case SOUTH -> {
|
||||
// 180°: (x,z) → (-x,-z)
|
||||
wMinX = cx - MODEL_MAX_X;
|
||||
wMaxX = cx - MODEL_MIN_X;
|
||||
wMinZ = cz - MODEL_MAX_Z;
|
||||
wMaxZ = cz - MODEL_MIN_Z;
|
||||
}
|
||||
case WEST -> {
|
||||
// 90°: (x,z) → (z,-x)
|
||||
wMinX = cx + MODEL_MIN_Z;
|
||||
wMaxX = cx + MODEL_MAX_Z;
|
||||
wMinZ = cz - MODEL_MAX_X;
|
||||
wMaxZ = cz - MODEL_MIN_X;
|
||||
}
|
||||
case EAST -> {
|
||||
// -90°: (x,z) → (-z,x)
|
||||
wMinX = cx - MODEL_MAX_Z;
|
||||
wMaxX = cx - MODEL_MIN_Z;
|
||||
wMinZ = cz + MODEL_MIN_X;
|
||||
wMaxZ = cz + MODEL_MAX_X;
|
||||
}
|
||||
default -> {
|
||||
// NORTH, 0°: (x,z) → (x,z)
|
||||
wMinX = cx + MODEL_MIN_X;
|
||||
wMaxX = cx + MODEL_MAX_X;
|
||||
wMinZ = cz + MODEL_MIN_Z;
|
||||
wMaxZ = cz + MODEL_MAX_Z;
|
||||
}
|
||||
}
|
||||
|
||||
return new double[] {
|
||||
wMinX,
|
||||
wMinZ,
|
||||
wMaxX,
|
||||
wMaxZ,
|
||||
masterPos.getY() + MODEL_MAX_Y,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Compute the cage collision VoxelShape for any block position within the cage grid.
|
||||
* Clips the 4 cage walls + floor to the block's boundaries.
|
||||
*/
|
||||
public static VoxelShape computeCageCollision(
|
||||
BlockPos masterPos,
|
||||
Direction facing,
|
||||
BlockPos blockPos
|
||||
) {
|
||||
double[] bounds = getCageWorldBounds(masterPos, facing);
|
||||
double cMinX = bounds[0],
|
||||
cMinZ = bounds[1];
|
||||
double cMaxX = bounds[2],
|
||||
cMaxZ = bounds[3];
|
||||
double cMaxY = bounds[4];
|
||||
double cMinY = masterPos.getY(); // Cage base = master Y
|
||||
|
||||
double bx = blockPos.getX();
|
||||
double by = blockPos.getY();
|
||||
double bz = blockPos.getZ();
|
||||
|
||||
VoxelShape shape = Shapes.empty();
|
||||
|
||||
// Left wall (at cMinX, perpendicular to X) — full height from base to ceiling
|
||||
shape = addClippedWall(
|
||||
shape,
|
||||
cMinX - WALL_T / 2,
|
||||
cMinY,
|
||||
cMinZ,
|
||||
cMinX + WALL_T / 2,
|
||||
cMaxY,
|
||||
cMaxZ,
|
||||
bx,
|
||||
by,
|
||||
bz
|
||||
);
|
||||
|
||||
// Right wall (at cMaxX)
|
||||
shape = addClippedWall(
|
||||
shape,
|
||||
cMaxX - WALL_T / 2,
|
||||
cMinY,
|
||||
cMinZ,
|
||||
cMaxX + WALL_T / 2,
|
||||
cMaxY,
|
||||
cMaxZ,
|
||||
bx,
|
||||
by,
|
||||
bz
|
||||
);
|
||||
|
||||
// Front wall (at cMinZ, perpendicular to Z)
|
||||
shape = addClippedWall(
|
||||
shape,
|
||||
cMinX,
|
||||
cMinY,
|
||||
cMinZ - WALL_T / 2,
|
||||
cMaxX,
|
||||
cMaxY,
|
||||
cMinZ + WALL_T / 2,
|
||||
bx,
|
||||
by,
|
||||
bz
|
||||
);
|
||||
|
||||
// Back wall (at cMaxZ)
|
||||
shape = addClippedWall(
|
||||
shape,
|
||||
cMinX,
|
||||
cMinY,
|
||||
cMaxZ - WALL_T / 2,
|
||||
cMaxX,
|
||||
cMaxY,
|
||||
cMaxZ + WALL_T / 2,
|
||||
bx,
|
||||
by,
|
||||
bz
|
||||
);
|
||||
|
||||
// Floor (at cage base only)
|
||||
shape = addClippedWall(
|
||||
shape,
|
||||
cMinX,
|
||||
cMinY,
|
||||
cMinZ,
|
||||
cMaxX,
|
||||
cMinY + WALL_T,
|
||||
cMaxZ,
|
||||
bx,
|
||||
by,
|
||||
bz
|
||||
);
|
||||
|
||||
// Ceiling: 1px thin at [1.9375, 2.0] — player head at 1.925 < 1.9375, works from both sides
|
||||
double ceilY = cMinY + 2.0;
|
||||
double ceilT = 0.0625; // 1 pixel thin so player (1.8 tall) fits under it
|
||||
shape = addClippedWall(
|
||||
shape,
|
||||
cMinX,
|
||||
ceilY - ceilT,
|
||||
cMinZ,
|
||||
cMaxX,
|
||||
ceilY,
|
||||
cMaxZ,
|
||||
bx,
|
||||
by,
|
||||
bz
|
||||
);
|
||||
|
||||
return shape;
|
||||
}
|
||||
|
||||
/**
|
||||
* Clip a world-space AABB to a block's local bounds and add it as a VoxelShape.
|
||||
* X and Z are clipped to [0,1] (entities in other blocks won't query this one).
|
||||
* Y max is allowed to extend above 1.0 (like fences) since entities below
|
||||
* will still query this block and collide with the extended shape.
|
||||
*/
|
||||
private static VoxelShape addClippedWall(
|
||||
VoxelShape existing,
|
||||
double wMinX,
|
||||
double wMinY,
|
||||
double wMinZ,
|
||||
double wMaxX,
|
||||
double wMaxY,
|
||||
double wMaxZ,
|
||||
double blockX,
|
||||
double blockY,
|
||||
double blockZ
|
||||
) {
|
||||
double lMinX = Math.max(0, wMinX - blockX);
|
||||
double lMinY = Math.max(0, wMinY - blockY);
|
||||
double lMinZ = Math.max(0, wMinZ - blockZ);
|
||||
double lMaxX = Math.min(1, wMaxX - blockX);
|
||||
double lMaxY = Math.min(1.5, wMaxY - blockY); // Allow extending up to 0.5 above block (like fences)
|
||||
double lMaxZ = Math.min(1, wMaxZ - blockZ);
|
||||
|
||||
if (lMinX >= lMaxX || lMinY >= lMaxY || lMinZ >= lMaxZ || lMaxY <= 0) {
|
||||
return existing;
|
||||
}
|
||||
|
||||
return Shapes.or(
|
||||
existing,
|
||||
Shapes.box(lMinX, lMinY, lMinZ, lMaxX, lMaxY, lMaxZ)
|
||||
);
|
||||
}
|
||||
|
||||
// ========================================
|
||||
// MULTI-BLOCK LAYOUT (3x3x2)
|
||||
// ========================================
|
||||
|
||||
private static final int[][] GRID_OFFSETS;
|
||||
|
||||
static {
|
||||
java.util.List<int[]> offsets = new java.util.ArrayList<>();
|
||||
for (int dy = 0; dy <= 1; dy++) {
|
||||
for (int dl = -1; dl <= 1; dl++) {
|
||||
for (int df = -1; df <= 1; df++) {
|
||||
if (dl == 0 && dy == 0 && df == 0) continue;
|
||||
offsets.add(new int[] { dl, dy, df });
|
||||
}
|
||||
}
|
||||
}
|
||||
GRID_OFFSETS = offsets.toArray(new int[0][]);
|
||||
}
|
||||
|
||||
public static BlockPos[] getPartPositions(
|
||||
BlockPos masterPos,
|
||||
Direction facing
|
||||
) {
|
||||
Direction left = facing.getCounterClockWise();
|
||||
|
||||
int lx = left.getStepX();
|
||||
int lz = left.getStepZ();
|
||||
int fx = facing.getStepX();
|
||||
int fz = facing.getStepZ();
|
||||
|
||||
BlockPos[] positions = new BlockPos[GRID_OFFSETS.length];
|
||||
for (int i = 0; i < GRID_OFFSETS.length; i++) {
|
||||
int dl = GRID_OFFSETS[i][0];
|
||||
int dy = GRID_OFFSETS[i][1];
|
||||
int df = GRID_OFFSETS[i][2];
|
||||
|
||||
int wx = dl * lx + df * fx;
|
||||
int wz = dl * lz + df * fz;
|
||||
positions[i] = masterPos.offset(wx, dy, wz);
|
||||
}
|
||||
return positions;
|
||||
}
|
||||
|
||||
@Nullable
|
||||
public static int[] getPartLocalOffset(
|
||||
BlockPos masterPos,
|
||||
Direction facing,
|
||||
BlockPos partPos
|
||||
) {
|
||||
Direction left = facing.getCounterClockWise();
|
||||
|
||||
int dx = partPos.getX() - masterPos.getX();
|
||||
int dy = partPos.getY() - masterPos.getY();
|
||||
int dz = partPos.getZ() - masterPos.getZ();
|
||||
|
||||
int lx = left.getStepX(),
|
||||
lz = left.getStepZ();
|
||||
int fx = facing.getStepX(),
|
||||
fz = facing.getStepZ();
|
||||
|
||||
int det = lx * fz - fx * lz;
|
||||
if (det == 0) return null;
|
||||
|
||||
int dl = (dx * fz - dz * fx) / det;
|
||||
int df = (dz * lx - dx * lz) / det;
|
||||
|
||||
if (
|
||||
dl < -1 || dl > 1 || df < -1 || df > 1 || dy < 0 || dy > 1
|
||||
) return null;
|
||||
if (dl == 0 && dy == 0 && df == 0) return null;
|
||||
|
||||
return new int[] { dl, dy, df };
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,129 @@
|
||||
package com.tiedup.remake.v2.blocks;
|
||||
|
||||
import com.tiedup.remake.core.TiedUpMod;
|
||||
import java.util.UUID;
|
||||
import net.minecraft.core.BlockPos;
|
||||
import net.minecraft.nbt.CompoundTag;
|
||||
import net.minecraft.resources.ResourceLocation;
|
||||
import net.minecraft.server.MinecraftServer;
|
||||
import net.minecraft.server.level.ServerPlayer;
|
||||
import net.minecraft.world.level.block.entity.BlockEntityType;
|
||||
import net.minecraft.world.level.block.state.BlockState;
|
||||
import net.minecraft.world.phys.AABB;
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
|
||||
/**
|
||||
* Block entity for Pet Cage.
|
||||
* Tracks the occupant UUID and provides OBJ model rendering data.
|
||||
*/
|
||||
public class PetCageBlockEntity extends ObjBlockEntity {
|
||||
|
||||
public static final ResourceLocation MODEL_LOCATION =
|
||||
ResourceLocation.fromNamespaceAndPath(
|
||||
TiedUpMod.MOD_ID,
|
||||
"models/obj/blocks/cage/model.obj"
|
||||
);
|
||||
|
||||
public static final ResourceLocation TEXTURE_LOCATION =
|
||||
ResourceLocation.fromNamespaceAndPath(
|
||||
TiedUpMod.MOD_ID,
|
||||
"textures/block/pet_cage.png"
|
||||
);
|
||||
|
||||
@Nullable
|
||||
private UUID occupantUUID;
|
||||
|
||||
public PetCageBlockEntity(
|
||||
BlockEntityType<?> type,
|
||||
BlockPos pos,
|
||||
BlockState state
|
||||
) {
|
||||
super(type, pos, state);
|
||||
}
|
||||
|
||||
@Override
|
||||
public ResourceLocation getModelLocation() {
|
||||
return MODEL_LOCATION;
|
||||
}
|
||||
|
||||
@Override
|
||||
public ResourceLocation getTextureLocation() {
|
||||
return TEXTURE_LOCATION;
|
||||
}
|
||||
|
||||
@Override
|
||||
public float getModelScale() {
|
||||
return 1.0f;
|
||||
}
|
||||
|
||||
/**
|
||||
* Large render bounding box so the cage doesn't disappear when the camera
|
||||
* is inside the model (e.g. player caged) or looking away from the master block.
|
||||
*/
|
||||
@Override
|
||||
public AABB getRenderBoundingBox() {
|
||||
BlockPos p = getBlockPos();
|
||||
return new AABB(
|
||||
p.getX() - 2,
|
||||
p.getY(),
|
||||
p.getZ() - 2,
|
||||
p.getX() + 3,
|
||||
p.getY() + 3,
|
||||
p.getZ() + 3
|
||||
);
|
||||
}
|
||||
|
||||
// ========================================
|
||||
// OCCUPANT MANAGEMENT
|
||||
// ========================================
|
||||
|
||||
public boolean hasOccupant() {
|
||||
return occupantUUID != null;
|
||||
}
|
||||
|
||||
@Nullable
|
||||
public UUID getOccupantUUID() {
|
||||
return occupantUUID;
|
||||
}
|
||||
|
||||
public void setOccupant(UUID uuid) {
|
||||
this.occupantUUID = uuid;
|
||||
setChanged();
|
||||
}
|
||||
|
||||
public void clearOccupant() {
|
||||
this.occupantUUID = null;
|
||||
setChanged();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the occupant ServerPlayer if online.
|
||||
*/
|
||||
@Nullable
|
||||
public ServerPlayer getOccupant(@Nullable MinecraftServer server) {
|
||||
if (server == null || occupantUUID == null) return null;
|
||||
return server.getPlayerList().getPlayer(occupantUUID);
|
||||
}
|
||||
|
||||
// ========================================
|
||||
// NBT
|
||||
// ========================================
|
||||
|
||||
@Override
|
||||
protected void saveAdditional(CompoundTag tag) {
|
||||
super.saveAdditional(tag);
|
||||
if (occupantUUID != null) {
|
||||
tag.putUUID("occupantUUID", occupantUUID);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void load(CompoundTag tag) {
|
||||
super.load(tag);
|
||||
if (tag.hasUUID("occupantUUID")) {
|
||||
this.occupantUUID = tag.getUUID("occupantUUID");
|
||||
} else {
|
||||
this.occupantUUID = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
189
src/main/java/com/tiedup/remake/v2/blocks/PetCageManager.java
Normal file
189
src/main/java/com/tiedup/remake/v2/blocks/PetCageManager.java
Normal file
@@ -0,0 +1,189 @@
|
||||
package com.tiedup.remake.v2.blocks;
|
||||
|
||||
import com.tiedup.remake.core.TiedUpMod;
|
||||
import java.util.Map;
|
||||
import java.util.UUID;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
import net.minecraft.core.BlockPos;
|
||||
import net.minecraft.server.level.ServerPlayer;
|
||||
import net.minecraft.world.entity.ai.attributes.AttributeInstance;
|
||||
import net.minecraft.world.entity.ai.attributes.AttributeModifier;
|
||||
import net.minecraft.world.entity.ai.attributes.Attributes;
|
||||
import net.minecraft.world.level.block.entity.BlockEntity;
|
||||
|
||||
/**
|
||||
* Server-side manager for pet cage confinement.
|
||||
* Tracks which players are caged, immobilizes them, and handles cleanup.
|
||||
*/
|
||||
public class PetCageManager {
|
||||
|
||||
private static final UUID PET_CAGE_SPEED_MODIFIER_UUID = UUID.fromString(
|
||||
"b2c3d4e5-f6a7-4890-bcde-f01234567890"
|
||||
);
|
||||
private static final String PET_CAGE_SPEED_MODIFIER_NAME =
|
||||
"tiedup.pet_cage_immobilize";
|
||||
private static final double FULL_IMMOBILIZATION = -0.10;
|
||||
|
||||
/** Active cage entries keyed by player UUID */
|
||||
private static final Map<UUID, CageEntry> entries =
|
||||
new ConcurrentHashMap<>();
|
||||
|
||||
private static class CageEntry {
|
||||
|
||||
final BlockPos pos;
|
||||
|
||||
CageEntry(BlockPos pos) {
|
||||
this.pos = pos;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Cage a player at the given master block position.
|
||||
* Teleports them to the center of the 3x3x3 cage and immobilizes.
|
||||
*/
|
||||
public static void cagePlayer(ServerPlayer player, BlockPos masterPos) {
|
||||
UUID uuid = player.getUUID();
|
||||
entries.put(uuid, new CageEntry(masterPos.immutable()));
|
||||
|
||||
// Teleport to cage center (master is at center of 3x3x3 grid)
|
||||
double[] center = getCageCenter(player, masterPos);
|
||||
player.teleportTo(center[0], center[1], center[2]);
|
||||
|
||||
// Immobilize
|
||||
applySpeedModifier(player);
|
||||
|
||||
TiedUpMod.LOGGER.debug(
|
||||
"[PetCageManager] {} caged at {}",
|
||||
player.getName().getString(),
|
||||
masterPos
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Release a player from cage confinement.
|
||||
*/
|
||||
public static void releasePlayer(ServerPlayer player) {
|
||||
UUID uuid = player.getUUID();
|
||||
CageEntry entry = entries.remove(uuid);
|
||||
if (entry == null) return;
|
||||
|
||||
// Release block entity
|
||||
BlockEntity be = player.level().getBlockEntity(entry.pos);
|
||||
if (be instanceof PetCageBlockEntity cageBE) {
|
||||
cageBE.clearOccupant();
|
||||
}
|
||||
|
||||
// Restore speed
|
||||
removeSpeedModifier(player);
|
||||
|
||||
TiedUpMod.LOGGER.debug(
|
||||
"[PetCageManager] {} released from cage at {}",
|
||||
player.getName().getString(),
|
||||
entry.pos
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if a player is in a cage.
|
||||
*/
|
||||
public static boolean isInCage(ServerPlayer player) {
|
||||
return entries.containsKey(player.getUUID());
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the cage position for a player, or null if not caged.
|
||||
*/
|
||||
@Nullable
|
||||
public static BlockPos getCagePos(ServerPlayer player) {
|
||||
CageEntry entry = entries.get(player.getUUID());
|
||||
return entry != null ? entry.pos : null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Tick a caged player to verify cage still exists.
|
||||
* Called from server-side tick handler.
|
||||
*/
|
||||
public static void tickPlayer(ServerPlayer player) {
|
||||
CageEntry entry = entries.get(player.getUUID());
|
||||
if (entry == null) return;
|
||||
|
||||
// Check if the block is still a cage
|
||||
if (
|
||||
!(player.level().getBlockState(entry.pos).getBlock() instanceof
|
||||
PetCageBlock)
|
||||
) {
|
||||
releasePlayer(player);
|
||||
return;
|
||||
}
|
||||
|
||||
// Keep player at cage center (prevent movement exploits)
|
||||
double[] center = getCageCenter(player, entry.pos);
|
||||
double distSq = player.distanceToSqr(center[0], center[1], center[2]);
|
||||
if (distSq > 0.25) {
|
||||
// > 0.5 blocks
|
||||
player.teleportTo(center[0], center[1], center[2]);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculate the center of the 3x3x3 cage from the master block position.
|
||||
* The master is at the center of the grid, so the cage center is the master block center.
|
||||
*/
|
||||
private static double[] getCageCenter(
|
||||
ServerPlayer player,
|
||||
BlockPos masterPos
|
||||
) {
|
||||
return new double[] {
|
||||
masterPos.getX() + 0.5,
|
||||
masterPos.getY(),
|
||||
masterPos.getZ() + 0.5,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Clean up when a player disconnects.
|
||||
*/
|
||||
public static void onPlayerDisconnect(UUID uuid) {
|
||||
entries.remove(uuid);
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove any leftover cage speed modifier on login.
|
||||
*/
|
||||
public static void onPlayerLogin(ServerPlayer player) {
|
||||
removeSpeedModifier(player);
|
||||
}
|
||||
|
||||
private static void applySpeedModifier(ServerPlayer player) {
|
||||
AttributeInstance movementSpeed = player.getAttribute(
|
||||
Attributes.MOVEMENT_SPEED
|
||||
);
|
||||
if (movementSpeed == null) return;
|
||||
|
||||
// Remove existing to avoid duplicates
|
||||
if (movementSpeed.getModifier(PET_CAGE_SPEED_MODIFIER_UUID) != null) {
|
||||
movementSpeed.removeModifier(PET_CAGE_SPEED_MODIFIER_UUID);
|
||||
}
|
||||
|
||||
AttributeModifier modifier = new AttributeModifier(
|
||||
PET_CAGE_SPEED_MODIFIER_UUID,
|
||||
PET_CAGE_SPEED_MODIFIER_NAME,
|
||||
FULL_IMMOBILIZATION,
|
||||
AttributeModifier.Operation.ADDITION
|
||||
);
|
||||
movementSpeed.addPermanentModifier(modifier);
|
||||
}
|
||||
|
||||
private static void removeSpeedModifier(ServerPlayer player) {
|
||||
AttributeInstance movementSpeed = player.getAttribute(
|
||||
Attributes.MOVEMENT_SPEED
|
||||
);
|
||||
if (
|
||||
movementSpeed != null &&
|
||||
movementSpeed.getModifier(PET_CAGE_SPEED_MODIFIER_UUID) != null
|
||||
) {
|
||||
movementSpeed.removeModifier(PET_CAGE_SPEED_MODIFIER_UUID);
|
||||
}
|
||||
}
|
||||
}
|
||||
169
src/main/java/com/tiedup/remake/v2/blocks/PetCagePartBlock.java
Normal file
169
src/main/java/com/tiedup/remake/v2/blocks/PetCagePartBlock.java
Normal file
@@ -0,0 +1,169 @@
|
||||
package com.tiedup.remake.v2.blocks;
|
||||
|
||||
import net.minecraft.core.BlockPos;
|
||||
import net.minecraft.core.Direction;
|
||||
import net.minecraft.world.InteractionHand;
|
||||
import net.minecraft.world.InteractionResult;
|
||||
import net.minecraft.world.entity.player.Player;
|
||||
import net.minecraft.world.level.BlockGetter;
|
||||
import net.minecraft.world.level.Level;
|
||||
import net.minecraft.world.level.block.Block;
|
||||
import net.minecraft.world.level.block.HorizontalDirectionalBlock;
|
||||
import net.minecraft.world.level.block.RenderShape;
|
||||
import net.minecraft.world.level.block.state.BlockState;
|
||||
import net.minecraft.world.level.block.state.StateDefinition;
|
||||
import net.minecraft.world.level.block.state.properties.DirectionProperty;
|
||||
import net.minecraft.world.phys.BlockHitResult;
|
||||
import net.minecraft.world.phys.shapes.CollisionContext;
|
||||
import net.minecraft.world.phys.shapes.Shapes;
|
||||
import net.minecraft.world.phys.shapes.VoxelShape;
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
|
||||
/**
|
||||
* Pet Cage Part Block (Slave) - Invisible structural part of a 3x3x2 multi-block cage.
|
||||
*
|
||||
* <p>No visible model, no selection outline. Provides collision shapes computed
|
||||
* from the actual OBJ model dimensions (delegated to {@link PetCageBlock#computeCageCollision}).
|
||||
* Interactions and breaking are delegated to the master.
|
||||
*/
|
||||
public class PetCagePartBlock extends Block {
|
||||
|
||||
public static final DirectionProperty FACING =
|
||||
HorizontalDirectionalBlock.FACING;
|
||||
|
||||
public PetCagePartBlock(Properties properties) {
|
||||
super(properties);
|
||||
this.registerDefaultState(
|
||||
this.stateDefinition.any().setValue(FACING, Direction.NORTH)
|
||||
);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void createBlockStateDefinition(
|
||||
StateDefinition.Builder<Block, BlockState> builder
|
||||
) {
|
||||
builder.add(FACING);
|
||||
}
|
||||
|
||||
@Override
|
||||
public VoxelShape getShape(
|
||||
BlockState state,
|
||||
BlockGetter level,
|
||||
BlockPos pos,
|
||||
CollisionContext context
|
||||
) {
|
||||
// Invisible: no outline, no selection box
|
||||
return Shapes.empty();
|
||||
}
|
||||
|
||||
@Override
|
||||
public VoxelShape getCollisionShape(
|
||||
BlockState state,
|
||||
BlockGetter level,
|
||||
BlockPos pos,
|
||||
CollisionContext context
|
||||
) {
|
||||
Direction facing = state.getValue(FACING);
|
||||
BlockPos masterPos = findMasterPos(state, level, pos);
|
||||
if (masterPos == null) return Shapes.empty();
|
||||
return PetCageBlock.computeCageCollision(masterPos, facing, pos);
|
||||
}
|
||||
|
||||
@Override
|
||||
public RenderShape getRenderShape(BlockState state) {
|
||||
return RenderShape.INVISIBLE;
|
||||
}
|
||||
|
||||
@Override
|
||||
public InteractionResult use(
|
||||
BlockState state,
|
||||
Level level,
|
||||
BlockPos pos,
|
||||
Player player,
|
||||
InteractionHand hand,
|
||||
BlockHitResult hit
|
||||
) {
|
||||
BlockPos masterPos = findMasterPos(state, level, pos);
|
||||
if (masterPos != null) {
|
||||
BlockState masterState = level.getBlockState(masterPos);
|
||||
if (masterState.getBlock() instanceof PetCageBlock cage) {
|
||||
return cage.use(
|
||||
masterState,
|
||||
level,
|
||||
masterPos,
|
||||
player,
|
||||
hand,
|
||||
hit
|
||||
);
|
||||
}
|
||||
}
|
||||
return InteractionResult.PASS;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onRemove(
|
||||
BlockState state,
|
||||
Level level,
|
||||
BlockPos pos,
|
||||
BlockState newState,
|
||||
boolean isMoving
|
||||
) {
|
||||
if (!state.is(newState.getBlock())) {
|
||||
BlockPos masterPos = findMasterPos(state, level, pos);
|
||||
if (masterPos != null) {
|
||||
BlockState masterState = level.getBlockState(masterPos);
|
||||
if (masterState.getBlock() instanceof PetCageBlock) {
|
||||
level.destroyBlock(masterPos, true);
|
||||
}
|
||||
}
|
||||
}
|
||||
super.onRemove(state, level, pos, newState, isMoving);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void playerWillDestroy(
|
||||
Level level,
|
||||
BlockPos pos,
|
||||
BlockState state,
|
||||
Player player
|
||||
) {
|
||||
// Don't call super to prevent double drops
|
||||
}
|
||||
|
||||
@Nullable
|
||||
private BlockPos findMasterPos(
|
||||
BlockState partState,
|
||||
BlockGetter level,
|
||||
BlockPos partPos
|
||||
) {
|
||||
Direction facing = partState.getValue(FACING);
|
||||
Direction left = facing.getCounterClockWise();
|
||||
|
||||
int lx = left.getStepX(),
|
||||
lz = left.getStepZ();
|
||||
int fx = facing.getStepX(),
|
||||
fz = facing.getStepZ();
|
||||
|
||||
for (int dy = 0; dy <= 1; dy++) {
|
||||
for (int dl = -1; dl <= 1; dl++) {
|
||||
for (int df = -1; df <= 1; df++) {
|
||||
if (dl == 0 && dy == 0 && df == 0) continue;
|
||||
|
||||
int wx = -(dl * lx + df * fx);
|
||||
int wz = -(dl * lz + df * fz);
|
||||
BlockPos candidate = partPos.offset(wx, -dy, wz);
|
||||
|
||||
BlockState candidateState = level.getBlockState(candidate);
|
||||
if (
|
||||
candidateState.getBlock() instanceof PetCageBlock &&
|
||||
candidateState.getValue(PetCageBlock.FACING) == facing
|
||||
) {
|
||||
return candidate;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user