diff --git a/src/main/java/com/trunksbomb/minetriad/blockentity/DuelTableBlockEntity.java b/src/main/java/com/trunksbomb/minetriad/blockentity/DuelTableBlockEntity.java index 4dce049..6824763 100644 --- a/src/main/java/com/trunksbomb/minetriad/blockentity/DuelTableBlockEntity.java +++ b/src/main/java/com/trunksbomb/minetriad/blockentity/DuelTableBlockEntity.java @@ -22,14 +22,23 @@ import net.minecraft.nbt.Tag; import net.minecraft.network.Connection; import net.minecraft.network.protocol.game.ClientboundBlockEntityDataPacket; import net.minecraft.network.chat.Component; +import net.minecraft.world.entity.EntityType; +import net.minecraft.world.entity.Interaction; import net.minecraft.world.ContainerHelper; import net.minecraft.world.item.ItemStack; import net.minecraft.world.level.Level; import net.minecraft.world.level.block.Block; import net.minecraft.world.level.block.entity.BlockEntity; import net.minecraft.world.level.block.state.BlockState; +import net.minecraft.world.phys.AABB; + +import com.trunksbomb.minetriad.game.BoardLocalSpace; +import com.trunksbomb.minetriad.world.DuelTableBlock; public class DuelTableBlockEntity extends BlockEntity { + public static final String REWARD_PROXY_TAG = "minetriad.reward_proxy"; + public static final String REWARD_PROXY_POS_PREFIX = "minetriad.reward_pos:"; + public static final String REWARD_PROXY_SLOT_PREFIX = "minetriad.reward_slot:"; public static final int SLOT_COUNT = 9; public static final int REWARD_SLOT_COUNT = 5; public static final int OWNER_NONE = 0; @@ -232,6 +241,7 @@ public class DuelTableBlockEntity extends BlockEntity { rewardCards.set(index, stack.copyWithCount(1)); } } + refreshRewardInteractions(); sync(); } @@ -244,6 +254,7 @@ public class DuelTableBlockEntity extends BlockEntity { return ItemStack.EMPTY; } rewardCards.set(slot, ItemStack.EMPTY); + refreshRewardInteractions(); sync(); return taken; } @@ -252,6 +263,7 @@ public class DuelTableBlockEntity extends BlockEntity { for (int index = 0; index < rewardCards.size(); index++) { rewardCards.set(index, ItemStack.EMPTY); } + removeRewardInteractions(); } public void dropRewardCards(Level level, BlockPos pos) { @@ -262,6 +274,76 @@ public class DuelTableBlockEntity extends BlockEntity { rewardCards.set(slot, ItemStack.EMPTY); } } + removeRewardInteractions(); + } + + public void refreshRewardInteractions() { + if (level == null || level.isClientSide) { + return; + } + removeRewardInteractions(); + int visibleIndex = 0; + var facing = getBlockState().getValue(DuelTableBlock.FACING); + for (int slot = 0; slot < rewardCards.size(); slot++) { + if (rewardCards.get(slot).isEmpty()) { + continue; + } + BoardLocalSpace.SlotCenter center = BoardLocalSpace.rewardCenter(visibleIndex, facing, rewardCardCount()); + Interaction interaction = new Interaction(EntityType.INTERACTION, level); + interaction.setPos(worldPosition.getX() + center.x(), worldPosition.getY() + 1.12D, worldPosition.getZ() + center.z()); + interaction.setNoGravity(true); + interaction.addTag(REWARD_PROXY_TAG); + interaction.addTag(REWARD_PROXY_POS_PREFIX + worldPosition.getX() + "," + worldPosition.getY() + "," + worldPosition.getZ()); + interaction.addTag(REWARD_PROXY_SLOT_PREFIX + slot); + level.addFreshEntity(interaction); + visibleIndex++; + } + } + + public void removeRewardInteractions() { + if (level == null || level.isClientSide) { + return; + } + AABB searchBox = new AABB(worldPosition).inflate(2.0D, 2.0D, 2.0D); + for (Interaction interaction : level.getEntitiesOfClass(Interaction.class, searchBox, DuelTableBlockEntity::isRewardProxy)) { + if (worldPosition.equals(rewardProxyPos(interaction))) { + interaction.discard(); + } + } + } + + public static boolean isRewardProxy(Interaction interaction) { + return interaction.getTags().contains(REWARD_PROXY_TAG); + } + + public static BlockPos rewardProxyPos(Interaction interaction) { + for (String tag : interaction.getTags()) { + if (!tag.startsWith(REWARD_PROXY_POS_PREFIX)) { + continue; + } + String[] parts = tag.substring(REWARD_PROXY_POS_PREFIX.length()).split(","); + if (parts.length == 3) { + try { + return new BlockPos(Integer.parseInt(parts[0]), Integer.parseInt(parts[1]), Integer.parseInt(parts[2])); + } catch (NumberFormatException ignored) { + return null; + } + } + } + return null; + } + + public static int rewardProxySlot(Interaction interaction) { + for (String tag : interaction.getTags()) { + if (tag.startsWith(REWARD_PROXY_SLOT_PREFIX)) { + try { + return Integer.parseInt(tag.substring(REWARD_PROXY_SLOT_PREFIX.length())); + } catch (NumberFormatException ignored) { + return -1; + } + } + } + return -1; } public void startMoveAnimation(MoveResult moveResult, int owner) { diff --git a/src/main/java/com/trunksbomb/minetriad/client/render/DuelTableBlockEntityRenderer.java b/src/main/java/com/trunksbomb/minetriad/client/render/DuelTableBlockEntityRenderer.java index 7cabf51..391c071 100644 --- a/src/main/java/com/trunksbomb/minetriad/client/render/DuelTableBlockEntityRenderer.java +++ b/src/main/java/com/trunksbomb/minetriad/client/render/DuelTableBlockEntityRenderer.java @@ -8,10 +8,14 @@ import com.trunksbomb.minetriad.game.BoardLocalSpace; import com.trunksbomb.minetriad.world.DuelTableBlock; import net.minecraft.client.Minecraft; +import net.minecraft.client.renderer.LevelRenderer; import net.minecraft.client.renderer.MultiBufferSource; +import net.minecraft.client.renderer.RenderType; import net.minecraft.client.renderer.blockentity.BlockEntityRenderer; import net.minecraft.client.renderer.blockentity.BlockEntityRendererProvider; +import net.minecraft.world.entity.Interaction; import net.minecraft.world.item.ItemStack; +import net.minecraft.world.phys.AABB; public class DuelTableBlockEntityRenderer implements BlockEntityRenderer { private static final float TABLE_CARD_Y = 1.066F; @@ -74,6 +78,7 @@ public class DuelTableBlockEntityRenderer implements BlockEntityRenderer DuelTableBlockEntity.isRewardProxy(entity) + && blockEntity.getBlockPos().equals(DuelTableBlockEntity.rewardProxyPos(entity)))) { + AABB box = interaction.getBoundingBox().move( + -blockEntity.getBlockPos().getX(), + -blockEntity.getBlockPos().getY(), + -blockEntity.getBlockPos().getZ()); + LevelRenderer.addChainedFilledBoxVertices( + poseStack, + buffer.getBuffer(RenderType.debugFilledBox()), + (float) box.minX, + (float) box.minY, + (float) box.minZ, + (float) box.maxX, + (float) box.maxY, + (float) box.maxZ, + 0.2F, + 0.95F, + 0.35F, + 0.28F); + } + } + private static TriadCardItemRenderer.CardPalette perspectivePalette(DuelTableBlockEntity blockEntity, int owner) { if (owner == DuelTableBlockEntity.OWNER_NONE || Minecraft.getInstance().player == null) { return new TriadCardItemRenderer.CardPalette( diff --git a/src/main/java/com/trunksbomb/minetriad/game/BoardLocalSpace.java b/src/main/java/com/trunksbomb/minetriad/game/BoardLocalSpace.java index ed5ae86..987701e 100644 --- a/src/main/java/com/trunksbomb/minetriad/game/BoardLocalSpace.java +++ b/src/main/java/com/trunksbomb/minetriad/game/BoardLocalSpace.java @@ -51,14 +51,27 @@ public final class BoardLocalSpace { public static int rewardIndexForHit(BlockHitResult hitResult, Direction boardFacing, int count) { double localX = hitResult.getLocation().x - hitResult.getBlockPos().getX(); + double localY = hitResult.getLocation().y - hitResult.getBlockPos().getY(); double localZ = hitResult.getLocation().z - hitResult.getBlockPos().getZ(); + if (localY < 1.20D || localY > 2.02D) { + return -1; + } + int nearestIndex = -1; + double nearestDistance = Double.MAX_VALUE; for (int index = 0; index < count; index++) { SlotCenter center = rewardCenter(index, boardFacing, count); - if (Math.abs(localX - center.x()) <= 0.07D && Math.abs(localZ - center.z()) <= 0.12D) { + double dx = localX - center.x(); + double dz = localZ - center.z(); + if (Math.abs(dx) <= 0.12D && Math.abs(dz) <= 0.18D) { return index; } + double distanceSq = dx * dx + dz * dz; + if (distanceSq < nearestDistance) { + nearestDistance = distanceSq; + nearestIndex = index; + } } - return -1; + return nearestDistance <= 0.05D * 0.05D ? nearestIndex : -1; } private static double projectProgress(double localX, double localZ, Direction direction) { diff --git a/src/main/java/com/trunksbomb/minetriad/world/DuelTableBlock.java b/src/main/java/com/trunksbomb/minetriad/world/DuelTableBlock.java index a6d4b66..1af81de 100644 --- a/src/main/java/com/trunksbomb/minetriad/world/DuelTableBlock.java +++ b/src/main/java/com/trunksbomb/minetriad/world/DuelTableBlock.java @@ -5,6 +5,7 @@ import com.trunksbomb.minetriad.MineTriad; import com.trunksbomb.minetriad.blockentity.DuelTableAnimationResolver; import com.trunksbomb.minetriad.blockentity.DuelTableBlockEntity; import com.trunksbomb.minetriad.card.CardStackData; +import com.trunksbomb.minetriad.game.BoardLocalSpace; import com.trunksbomb.minetriad.game.DuelSession; import com.trunksbomb.minetriad.game.DuelSessionManager; import com.trunksbomb.minetriad.game.MoveResult; @@ -20,8 +21,10 @@ import net.minecraft.world.InteractionHand; import net.minecraft.world.InteractionResult; import net.minecraft.world.ItemInteractionResult; import net.minecraft.world.entity.player.Player; +import net.minecraft.world.entity.Interaction; import net.minecraft.world.item.ItemStack; import net.minecraft.world.level.Level; +import net.minecraft.world.level.BlockGetter; import net.minecraft.world.level.block.BaseEntityBlock; import net.minecraft.world.level.block.HorizontalDirectionalBlock; import net.minecraft.world.level.block.RenderShape; @@ -34,6 +37,12 @@ import net.minecraft.world.level.block.state.BlockState; import net.minecraft.world.level.block.state.properties.BlockStateProperties; 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 net.neoforged.bus.api.SubscribeEvent; +import net.neoforged.fml.common.EventBusSubscriber; +import net.neoforged.neoforge.event.entity.player.PlayerInteractEvent; public class DuelTableBlock extends BaseEntityBlock { public static final MapCodec CODEC = simpleCodec(DuelTableBlock::new); @@ -70,8 +79,10 @@ public class DuelTableBlock extends BaseEntityBlock { return ItemInteractionResult.CONSUME; } if (hitResult.getDirection() != Direction.UP) { + player.displayClientMessage(Component.literal("Reward debug: no reward hit; clicked face " + hitResult.getDirection().getName()).withStyle(ChatFormatting.DARK_GRAY), false); return ItemInteractionResult.CONSUME; } + player.displayClientMessage(Component.literal("Reward debug: no reward hit; falling back to board close.").withStyle(ChatFormatting.DARK_GRAY), false); completeOverview(level, pos, player, session); return ItemInteractionResult.CONSUME; } @@ -191,6 +202,54 @@ public class DuelTableBlock extends BaseEntityBlock { return RenderShape.MODEL; } + @Override + protected VoxelShape getShape(BlockState state, BlockGetter level, BlockPos pos, CollisionContext context) { + return overviewRewardShape(state, level, pos, super.getShape(state, level, pos, context)); + } + + @Override + protected VoxelShape getInteractionShape(BlockState state, BlockGetter level, BlockPos pos) { + return overviewRewardShape(state, level, pos, super.getInteractionShape(state, level, pos)); + } + + private VoxelShape overviewRewardShape(BlockState state, BlockGetter level, BlockPos pos, VoxelShape baseShape) { + VoxelShape shape = baseShape; + BlockEntity blockEntity = level.getBlockEntity(pos); + if (!(blockEntity instanceof DuelTableBlockEntity table) || table.rewardCardCount() <= 0) { + return shape; + } + + Direction tableFacing = state.getValue(FACING); + int rewardCount = table.rewardCardCount(); + VoxelShape rewardShape = Shapes.empty(); + for (int index = 0; index < rewardCount; index++) { + BoardLocalSpace.SlotCenter center = BoardLocalSpace.rewardCenter(index, tableFacing, rewardCount); + double minX; + double maxX; + double minZ; + double maxZ; + if (tableFacing.getAxis() == Direction.Axis.Z) { + minX = center.x() - 0.11D; + maxX = center.x() + 0.11D; + minZ = center.z() - 0.05D; + maxZ = center.z() + 0.05D; + } else { + minX = center.x() - 0.05D; + maxX = center.x() + 0.05D; + minZ = center.z() - 0.11D; + maxZ = center.z() + 0.11D; + } + rewardShape = Shapes.or(rewardShape, Block.box( + minX * 16.0D, + 20.0D, + minZ * 16.0D, + maxX * 16.0D, + 31.5D, + maxZ * 16.0D)); + } + return Shapes.or(shape, rewardShape); + } + @Override protected void createBlockStateDefinition(StateDefinition.Builder builder) { builder.add(FACING); @@ -232,24 +291,37 @@ public class DuelTableBlock extends BaseEntityBlock { private static boolean tryTakeRewardCard(Level level, BlockPos pos, Player player, BlockHitResult hitResult, Direction tableFacing) { DuelTableBlockEntity table = getTableEntity(level, pos); if (table == null || table.rewardCardCount() <= 0) { + player.displayClientMessage(Component.literal("Reward debug: no reward cards available.").withStyle(ChatFormatting.DARK_GRAY), false); return false; } + double localX = hitResult.getLocation().x - hitResult.getBlockPos().getX(); + double localY = hitResult.getLocation().y - hitResult.getBlockPos().getY(); + double localZ = hitResult.getLocation().z - hitResult.getBlockPos().getZ(); int rewardIndex = com.trunksbomb.minetriad.game.BoardLocalSpace.rewardIndexForHit(hitResult, tableFacing, table.rewardCardCount()); if (rewardIndex < 0) { + player.displayClientMessage(Component.literal(String.format( + "Reward debug: miss at local %.3f, %.3f, %.3f with %d cards", + localX, + localY, + localZ, + table.rewardCardCount())).withStyle(ChatFormatting.DARK_GRAY), false); return false; } int actualSlot = actualRewardSlot(table, rewardIndex); if (actualSlot < 0) { + player.displayClientMessage(Component.literal("Reward debug: resolved visual index " + rewardIndex + " but found no matching slot.").withStyle(ChatFormatting.DARK_GRAY), false); return false; } ItemStack reward = table.takeRewardCard(actualSlot); if (reward.isEmpty()) { + player.displayClientMessage(Component.literal("Reward debug: slot " + actualSlot + " was empty when taking.").withStyle(ChatFormatting.DARK_GRAY), false); return false; } + player.displayClientMessage(Component.literal("Reward debug: took visual index " + rewardIndex + " from slot " + actualSlot + ".").withStyle(ChatFormatting.DARK_GRAY), false); return true; } @@ -266,4 +338,42 @@ public class DuelTableBlock extends BaseEntityBlock { } return -1; } + + @EventBusSubscriber(modid = MineTriad.MOD_ID) + public static final class RewardInteractionEvents { + @SubscribeEvent + public static void onEntityInteract(PlayerInteractEvent.EntityInteractSpecific event) { + if (!(event.getTarget() instanceof Interaction interaction) || !DuelTableBlockEntity.isRewardProxy(interaction)) { + return; + } + + BlockPos tablePos = DuelTableBlockEntity.rewardProxyPos(interaction); + if (tablePos == null) { + return; + } + + Level level = event.getLevel(); + DuelTableBlockEntity table = getTableEntity(level, tablePos); + DuelSession session = DuelSessionManager.getAt(tablePos); + if (table == null || session == null || !session.isInOverview()) { + interaction.discard(); + return; + } + + int slot = DuelTableBlockEntity.rewardProxySlot(interaction); + if (slot < 0) { + return; + } + + ItemStack reward = table.takeRewardCard(slot); + if (reward.isEmpty()) { + interaction.discard(); + return; + } + + event.getEntity().displayClientMessage(Component.literal("Reward debug: entity take from slot " + slot + ".").withStyle(ChatFormatting.DARK_GRAY), false); + event.setCanceled(true); + event.setCancellationResult(InteractionResult.SUCCESS); + } + } }