From e5d3b5d233fb536ff34a230f89bd4eb7194c2bdb Mon Sep 17 00:00:00 2001 From: trunksbomb Date: Mon, 23 Mar 2026 13:05:18 -0400 Subject: [PATCH] initial pass on the post-game card reward screen. The loser's cards float up above the board to be taken by the winner. Just mockup right now - it's always player 2's cards and you don't actually take them. --- .../DuelTableAnimationResolver.java | 19 +--- .../blockentity/DuelTableBlockEntity.java | 88 +++++++++++++++++++ .../render/DuelTableBlockEntityRenderer.java | 37 +++++++- .../minetriad/game/BoardLocalSpace.java | 25 ++++++ .../minetriad/game/DuelSession.java | 10 ++- .../minetriad/world/DuelTableBlock.java | 81 +++++++++++------ 6 files changed, 218 insertions(+), 42 deletions(-) diff --git a/src/main/java/com/trunksbomb/minetriad/blockentity/DuelTableAnimationResolver.java b/src/main/java/com/trunksbomb/minetriad/blockentity/DuelTableAnimationResolver.java index 02906e4..dc8dcae 100644 --- a/src/main/java/com/trunksbomb/minetriad/blockentity/DuelTableAnimationResolver.java +++ b/src/main/java/com/trunksbomb/minetriad/blockentity/DuelTableAnimationResolver.java @@ -49,8 +49,6 @@ public final class DuelTableAnimationResolver { session.beginOpponentAnimation(); table.startMoveAnimation(opponentMove, DuelTableBlockEntity.OWNER_SECOND); - player.displayClientMessage(Component.literal("Training Duelist answers with " + opponentMove.playedCardName() + ".").withStyle(ChatFormatting.GRAY), false); - sendBattleLog(player, opponentMove, ChatFormatting.GRAY); } static void handleAfterOpponentAnimation(Level level, BlockPos pos, DuelTableBlockEntity table, DuelSession session) { @@ -70,9 +68,6 @@ public final class DuelTableAnimationResolver { } session.returnToPlaying(); - player.displayClientMessage(session.boardSummary(), false); - player.displayClientMessage(Component.literal("Your turn. Hold one of your remaining duel cards and click an open space.").withStyle(ChatFormatting.YELLOW), false); - player.displayClientMessage(session.handSummary(), false); } public static void startAiTurn(DuelTableBlockEntity table, DuelSession session, ServerPlayer player) { @@ -98,22 +93,16 @@ public final class DuelTableAnimationResolver { session.beginOpponentAnimation(); } table.startMoveAnimation(aiMove, owner); - player.displayClientMessage(Component.literal((owner == DuelTableBlockEntity.OWNER_FIRST ? "Blue AI" : "Red AI") + " plays " + aiMove.playedCardName() + ".") - .withStyle(ChatFormatting.GRAY), false); - sendBattleLog(player, aiMove, ChatFormatting.GRAY); } private static void enterOverview(DuelTableBlockEntity table, DuelSession session, ServerPlayer player) { session.enterOverview(); + table.setRewardCards(session.opponentRewardCards().stream() + .map(cardId -> CardItem.createCardStack(cardId, TriadItems.TRIAD_CARD.get())) + .toList()); + table.removeBoardCardsOwnedBy(DuelTableBlockEntity.OWNER_SECOND); table.startOverviewSweepAnimation(); player.displayClientMessage(session.resultSummary().copy().withStyle(ChatFormatting.AQUA), false); - player.displayClientMessage(session.overviewMessage().copy().withStyle(ChatFormatting.YELLOW), false); - } - - private static void sendBattleLog(ServerPlayer player, MoveResult moveResult, ChatFormatting color) { - for (String line : moveResult.battleLog()) { - player.displayClientMessage(Component.literal(line).withStyle(color), false); - } } private static ServerPlayer getPlayer(Level level, DuelSession session) { diff --git a/src/main/java/com/trunksbomb/minetriad/blockentity/DuelTableBlockEntity.java b/src/main/java/com/trunksbomb/minetriad/blockentity/DuelTableBlockEntity.java index 3ea1a19..4dce049 100644 --- a/src/main/java/com/trunksbomb/minetriad/blockentity/DuelTableBlockEntity.java +++ b/src/main/java/com/trunksbomb/minetriad/blockentity/DuelTableBlockEntity.java @@ -18,6 +18,7 @@ import net.minecraft.core.BlockPos; import net.minecraft.core.HolderLookup; import net.minecraft.core.NonNullList; import net.minecraft.nbt.CompoundTag; +import net.minecraft.nbt.Tag; import net.minecraft.network.Connection; import net.minecraft.network.protocol.game.ClientboundBlockEntityDataPacket; import net.minecraft.network.chat.Component; @@ -30,6 +31,7 @@ import net.minecraft.world.level.block.state.BlockState; public class DuelTableBlockEntity extends BlockEntity { public static final int SLOT_COUNT = 9; + public static final int REWARD_SLOT_COUNT = 5; public static final int OWNER_NONE = 0; public static final int OWNER_FIRST = 1; public static final int OWNER_SECOND = 2; @@ -39,6 +41,7 @@ public class DuelTableBlockEntity extends BlockEntity { private static final int CLEAR_WAVE_DURATION = 10; private final NonNullList boardCards = NonNullList.withSize(SLOT_COUNT, ItemStack.EMPTY); + private final NonNullList rewardCards = NonNullList.withSize(REWARD_SLOT_COUNT, ItemStack.EMPTY); private final int[] ownerSlots = new int[SLOT_COUNT]; private final int[] slotAges = new int[SLOT_COUNT]; private final ArrayDeque pendingAnimations = new ArrayDeque<>(); @@ -62,6 +65,20 @@ public class DuelTableBlockEntity extends BlockEntity { return boardCards.get(slot); } + public ItemStack getRewardCard(int slot) { + return slot >= 0 && slot < rewardCards.size() ? rewardCards.get(slot) : ItemStack.EMPTY; + } + + public int rewardCardCount() { + int count = 0; + for (ItemStack rewardCard : rewardCards) { + if (!rewardCard.isEmpty()) { + count++; + } + } + return count; + } + public int ownerAt(int slot) { return ownerSlots[slot]; } @@ -167,11 +184,28 @@ public class DuelTableBlockEntity extends BlockEntity { } pendingAnimations.clear(); clearAnimationState(); + clearRewardCards(); firstParticipantId = null; secondParticipantId = null; sync(); } + public void removeBoardCardsOwnedBy(int owner) { + boolean changed = false; + for (int index = 0; index < boardCards.size(); index++) { + if (ownerSlots[index] != owner || boardCards.get(index).isEmpty()) { + continue; + } + boardCards.set(index, ItemStack.EMPTY); + ownerSlots[index] = OWNER_NONE; + slotAges[index] = 0; + changed = true; + } + if (changed) { + sync(); + } + } + public void dropBoardCards(Level level, BlockPos pos) { for (int slot = 0; slot < boardCards.size(); slot++) { ItemStack stack = boardCards.get(slot); @@ -184,11 +218,52 @@ public class DuelTableBlockEntity extends BlockEntity { } pendingAnimations.clear(); clearAnimationState(); + dropRewardCards(level, pos); firstParticipantId = null; secondParticipantId = null; sync(); } + public void setRewardCards(List stacks) { + clearRewardCards(); + for (int index = 0; index < rewardCards.size() && index < stacks.size(); index++) { + ItemStack stack = stacks.get(index); + if (!stack.isEmpty()) { + rewardCards.set(index, stack.copyWithCount(1)); + } + } + sync(); + } + + public ItemStack takeRewardCard(int slot) { + if (slot < 0 || slot >= rewardCards.size()) { + return ItemStack.EMPTY; + } + ItemStack taken = rewardCards.get(slot); + if (taken.isEmpty()) { + return ItemStack.EMPTY; + } + rewardCards.set(slot, ItemStack.EMPTY); + sync(); + return taken; + } + + public void clearRewardCards() { + for (int index = 0; index < rewardCards.size(); index++) { + rewardCards.set(index, ItemStack.EMPTY); + } + } + + public void dropRewardCards(Level level, BlockPos pos) { + for (int slot = 0; slot < rewardCards.size(); slot++) { + ItemStack stack = rewardCards.get(slot); + if (!stack.isEmpty()) { + Block.popResource(level, pos, stack); + rewardCards.set(slot, ItemStack.EMPTY); + } + } + } + public void startMoveAnimation(MoveResult moveResult, int owner) { pendingAnimations.clear(); clearAnimationState(); @@ -280,6 +355,9 @@ public class DuelTableBlockEntity extends BlockEntity { protected void saveAdditional(CompoundTag tag, HolderLookup.Provider registries) { super.saveAdditional(tag, registries); ContainerHelper.saveAllItems(tag, boardCards, registries); + CompoundTag rewardTag = new CompoundTag(); + ContainerHelper.saveAllItems(rewardTag, rewardCards, registries); + tag.put("RewardCards", rewardTag); tag.putIntArray("OwnerSlots", ownerSlots); tag.putIntArray("SlotAges", slotAges); saveAnimation(tag); @@ -297,6 +375,9 @@ public class DuelTableBlockEntity extends BlockEntity { super.loadAdditional(tag, registries); clearBoardContents(); ContainerHelper.loadAllItems(tag, boardCards, registries); + if (tag.contains("RewardCards", Tag.TAG_COMPOUND)) { + ContainerHelper.loadAllItems(tag.getCompound("RewardCards"), rewardCards, registries); + } loadOwnerData(tag); loadAnimation(tag); loadTableAnimation(tag); @@ -306,6 +387,9 @@ public class DuelTableBlockEntity extends BlockEntity { public CompoundTag getUpdateTag(HolderLookup.Provider registries) { CompoundTag tag = super.getUpdateTag(registries); ContainerHelper.saveAllItems(tag, boardCards, registries); + CompoundTag rewardTag = new CompoundTag(); + ContainerHelper.saveAllItems(rewardTag, rewardCards, registries); + tag.put("RewardCards", rewardTag); tag.putIntArray("OwnerSlots", ownerSlots); tag.putIntArray("SlotAges", slotAges); saveAnimation(tag); @@ -330,6 +414,9 @@ public class DuelTableBlockEntity extends BlockEntity { if (tag != null) { clearBoardContents(); ContainerHelper.loadAllItems(tag, boardCards, registries); + if (tag.contains("RewardCards", Tag.TAG_COMPOUND)) { + ContainerHelper.loadAllItems(tag.getCompound("RewardCards"), rewardCards, registries); + } loadOwnerData(tag); loadAnimation(tag); loadTableAnimation(tag); @@ -344,6 +431,7 @@ public class DuelTableBlockEntity extends BlockEntity { } pendingAnimations.clear(); clearAnimationState(); + clearRewardCards(); firstParticipantId = null; secondParticipantId = null; } 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 d223235..7cabf51 100644 --- a/src/main/java/com/trunksbomb/minetriad/client/render/DuelTableBlockEntityRenderer.java +++ b/src/main/java/com/trunksbomb/minetriad/client/render/DuelTableBlockEntityRenderer.java @@ -15,6 +15,7 @@ import net.minecraft.world.item.ItemStack; public class DuelTableBlockEntityRenderer implements BlockEntityRenderer { private static final float TABLE_CARD_Y = 1.066F; + private static final float REWARD_CARD_Y = 1.75F; private static final float IDLE_BOB_HEIGHT = 0.012F; private static final float IDLE_BOB_SPEED = 0.08F; @@ -44,7 +45,7 @@ public class DuelTableBlockEntityRenderer implements BlockEntityRenderer refundablePlayerCards; + private final List opponentStartingCards; private final boolean refundCardsOnEnd; private final boolean aiVsAi; private final BlockPos tablePos; @@ -46,6 +47,9 @@ public final class DuelSession { this.opponentParticipant = new MatchParticipant(UUID.nameUUIDFromBytes(("opponent:" + playerId).getBytes()), "Training Duelist"); this.match = new TriadMatch(playerParticipant, opponentParticipant, playerHand, opponentHand, TriadRuleSet.CLASSIC_OPEN); this.refundablePlayerCards = List.copyOf(refundablePlayerCards); + this.opponentStartingCards = opponentHand.stream() + .map(card -> card.definition().id()) + .toList(); this.refundCardsOnEnd = refundCardsOnEnd; this.aiVsAi = aiVsAi; this.tablePos = tablePos.immutable(); @@ -210,7 +214,7 @@ public final class DuelSession { } public Component overviewMessage() { - return Component.literal("Duel overview. Right-click the table to finish and return cards."); + return Component.literal("Duel overview. Right-click a floating red card to take it, or right-click the table to finish."); } public List refundablePlayerCards() { @@ -245,6 +249,10 @@ public final class DuelSession { return refundCardsOnEnd; } + public List opponentRewardCards() { + return List.copyOf(opponentStartingCards); + } + public boolean isAtTable(BlockPos blockPos) { return tablePos.equals(blockPos); } diff --git a/src/main/java/com/trunksbomb/minetriad/world/DuelTableBlock.java b/src/main/java/com/trunksbomb/minetriad/world/DuelTableBlock.java index 4658b4c..a6d4b66 100644 --- a/src/main/java/com/trunksbomb/minetriad/world/DuelTableBlock.java +++ b/src/main/java/com/trunksbomb/minetriad/world/DuelTableBlock.java @@ -56,19 +56,31 @@ public class DuelTableBlock extends BaseEntityBlock { } try { - if (hitResult.getDirection() != Direction.UP) { - player.displayClientMessage(Component.literal("Click the top face of the Duel Table to target a board space.").withStyle(ChatFormatting.YELLOW), false); - return ItemInteractionResult.CONSUME; - } - Direction tableFacing = state.getValue(FACING); - DuelTableBlockEntity table = getTableEntity(level, pos); if (table == null) { player.displayClientMessage(Component.literal("Duel table storage is unavailable.").withStyle(ChatFormatting.RED), false); return ItemInteractionResult.CONSUME; } + Direction tableFacing = state.getValue(FACING); + DuelSession session = DuelSessionManager.get(player); + if (session != null && session.isInOverview()) { + if (tryTakeRewardCard(level, pos, player, hitResult, tableFacing)) { + return ItemInteractionResult.CONSUME; + } + if (hitResult.getDirection() != Direction.UP) { + return ItemInteractionResult.CONSUME; + } + completeOverview(level, pos, player, session); + return ItemInteractionResult.CONSUME; + } + + if (hitResult.getDirection() != Direction.UP) { + player.displayClientMessage(Component.literal("Click the top face of the Duel Table to target a board space.").withStyle(ChatFormatting.YELLOW), false); + return ItemInteractionResult.CONSUME; + } + if (session == null) { boolean aiVsAi = stack.isEmpty(); try { @@ -86,24 +98,15 @@ public class DuelTableBlock extends BaseEntityBlock { } } else { player.displayClientMessage(session.startMessage().copy().withStyle(ChatFormatting.GOLD), false); - player.displayClientMessage(session.handSummary(), false); } return ItemInteractionResult.CONSUME; } - if (session.isInOverview()) { - completeOverview(level, pos, player, session); - return ItemInteractionResult.CONSUME; - } - if (table.hasActiveAnimation() || session.isAnimating()) { - player.displayClientMessage(Component.literal("Wait for the current card animation to finish.").withStyle(ChatFormatting.YELLOW), false); return ItemInteractionResult.CONSUME; } if (!stack.is(TriadItems.TRIAD_CARD.get())) { - player.displayClientMessage(Component.literal("Hold a Triad Card from your duel hand to play your turn.").withStyle(ChatFormatting.YELLOW), false); - player.displayClientMessage(session.handSummary(), false); return ItemInteractionResult.CONSUME; } @@ -116,8 +119,6 @@ public class DuelTableBlock extends BaseEntityBlock { MoveResult playerMove = session.playPlayerCard(cardData.cardId(), hitResult, player.getAbilities().instabuild, tableFacing); if (!playerMove.valid()) { player.displayClientMessage(Component.literal("Move rejected: " + playerMove.errorMessage()).withStyle(ChatFormatting.RED), false); - player.displayClientMessage(session.boardSummary().copy().withStyle(ChatFormatting.DARK_GRAY), false); - player.displayClientMessage(session.handSummary(), false); return ItemInteractionResult.CONSUME; } @@ -133,8 +134,6 @@ public class DuelTableBlock extends BaseEntityBlock { session.beginPlayerAnimation(); table.startMoveAnimation(playerMove, DuelTableBlockEntity.OWNER_FIRST); - player.displayClientMessage(Component.literal("You play " + playerMove.playedCardName() + "."), false); - sendBattleLog(player, playerMove, ChatFormatting.DARK_AQUA); return ItemInteractionResult.CONSUME; } catch (Exception exception) { MineTriad.LOGGER.error("Duel table interaction failed", exception); @@ -209,12 +208,6 @@ public class DuelTableBlock extends BaseEntityBlock { return table.setCard(moveResult.playedCell().index(), CardItem.createCardStack(moveResult.playedCardId(), TriadItems.TRIAD_CARD.get()), owner); } - private static void sendBattleLog(Player player, MoveResult moveResult, ChatFormatting color) { - for (String line : moveResult.battleLog()) { - player.displayClientMessage(Component.literal(line).withStyle(color), false); - } - } - private static DuelTableBlockEntity getTableEntity(Level level, BlockPos pos) { BlockEntity blockEntity = level.getBlockEntity(pos); return blockEntity instanceof DuelTableBlockEntity duelTableBlockEntity ? duelTableBlockEntity : null; @@ -235,4 +228,42 @@ public class DuelTableBlock extends BaseEntityBlock { table.startClearWaveAnimation(); } } + + 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) { + return false; + } + + int rewardIndex = com.trunksbomb.minetriad.game.BoardLocalSpace.rewardIndexForHit(hitResult, tableFacing, table.rewardCardCount()); + if (rewardIndex < 0) { + return false; + } + + int actualSlot = actualRewardSlot(table, rewardIndex); + if (actualSlot < 0) { + return false; + } + + ItemStack reward = table.takeRewardCard(actualSlot); + if (reward.isEmpty()) { + return false; + } + + return true; + } + + private static int actualRewardSlot(DuelTableBlockEntity table, int rewardIndex) { + int visibleIndex = 0; + for (int slot = 0; slot < DuelTableBlockEntity.REWARD_SLOT_COUNT; slot++) { + if (table.getRewardCard(slot).isEmpty()) { + continue; + } + if (visibleIndex == rewardIndex) { + return slot; + } + visibleIndex++; + } + return -1; + } }