cards appear in "Reward row" above the table after victory and are clickable. There are some quirks to work out but it's good enough for now.

This commit is contained in:
trunksbomb
2026-03-23 16:27:04 -04:00
parent 109d832ac7
commit a60b41a774
4 changed files with 243 additions and 2 deletions

View File

@@ -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) {

View File

@@ -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<DuelTableBlockEntity> {
private static final float TABLE_CARD_Y = 1.066F;
@@ -74,6 +78,7 @@ public class DuelTableBlockEntityRenderer implements BlockEntityRenderer<DuelTab
}
renderRewardCards(blockEntity, partialTick, poseStack, buffer);
renderRewardProxyDebug(blockEntity, poseStack, buffer);
}
private static void renderRewardCards(DuelTableBlockEntity blockEntity, float partialTick, PoseStack poseStack, MultiBufferSource buffer) {
@@ -123,6 +128,37 @@ public class DuelTableBlockEntityRenderer implements BlockEntityRenderer<DuelTab
return Math.floorMod(blockEntity.getBlockPos().hashCode() * 31 + slot * 17, 97);
}
private static void renderRewardProxyDebug(DuelTableBlockEntity blockEntity, PoseStack poseStack, MultiBufferSource buffer) {
if (Minecraft.getInstance().level == null) {
return;
}
AABB searchBox = new AABB(blockEntity.getBlockPos()).inflate(2.0D, 2.0D, 2.0D);
for (Interaction interaction : Minecraft.getInstance().level.getEntitiesOfClass(
Interaction.class,
searchBox,
entity -> 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(

View File

@@ -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) {

View File

@@ -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<DuelTableBlock> 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<Block, BlockState> 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);
}
}
}