Compare commits
8 Commits
4cd60e4fac
...
master
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
905bb986f1 | ||
|
|
95d98fb721 | ||
|
|
b845d76cd0 | ||
|
|
a60b41a774 | ||
|
|
109d832ac7 | ||
|
|
e5d3b5d233 | ||
|
|
316ffebe1f | ||
|
|
261f540317 |
@@ -1,5 +1,6 @@
|
||||
package com.trunksbomb.minetriad;
|
||||
|
||||
import com.trunksbomb.minetriad.client.hud.LocalDuelHudOverlay;
|
||||
import com.trunksbomb.minetriad.client.render.FirstPersonCardHandRenderer;
|
||||
import com.trunksbomb.minetriad.client.render.DuelTableBlockEntityRenderer;
|
||||
import com.trunksbomb.minetriad.client.screen.CardBinderScreen;
|
||||
@@ -16,7 +17,9 @@ import net.neoforged.bus.api.IEventBus;
|
||||
import net.neoforged.fml.common.EventBusSubscriber;
|
||||
import net.neoforged.neoforge.client.event.EntityRenderersEvent;
|
||||
import net.neoforged.neoforge.client.event.RegisterMenuScreensEvent;
|
||||
import net.neoforged.neoforge.client.event.ClientTickEvent;
|
||||
import net.neoforged.neoforge.client.event.RenderHandEvent;
|
||||
import net.neoforged.neoforge.client.event.RenderGuiLayerEvent;
|
||||
|
||||
@Mod(value = MineTriad.MOD_ID, dist = Dist.CLIENT)
|
||||
public final class MineTriadClient {
|
||||
@@ -55,5 +58,15 @@ public final class MineTriadClient {
|
||||
event.getEquipProgress(),
|
||||
event.getItemStack());
|
||||
}
|
||||
|
||||
@SubscribeEvent
|
||||
public static void onClientTick(ClientTickEvent.Post event) {
|
||||
LocalDuelHudOverlay.onClientTick(event);
|
||||
}
|
||||
|
||||
@SubscribeEvent
|
||||
public static void onRenderGui(RenderGuiLayerEvent.Post event) {
|
||||
LocalDuelHudOverlay.onRenderGui(event);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,115 @@
|
||||
package com.trunksbomb.minetriad.blockentity;
|
||||
|
||||
import com.trunksbomb.minetriad.game.DuelSession;
|
||||
import com.trunksbomb.minetriad.game.DuelSessionManager;
|
||||
import com.trunksbomb.minetriad.game.MoveResult;
|
||||
import com.trunksbomb.minetriad.item.CardItem;
|
||||
import com.trunksbomb.minetriad.registry.TriadItems;
|
||||
|
||||
import net.minecraft.ChatFormatting;
|
||||
import net.minecraft.core.BlockPos;
|
||||
import net.minecraft.network.chat.Component;
|
||||
import net.minecraft.server.level.ServerPlayer;
|
||||
import net.minecraft.world.level.Level;
|
||||
|
||||
public final class DuelTableAnimationResolver {
|
||||
private DuelTableAnimationResolver() {
|
||||
}
|
||||
|
||||
static void handleAfterPlayerAnimation(Level level, BlockPos pos, DuelTableBlockEntity table, DuelSession session) {
|
||||
ServerPlayer player = getPlayer(level, session);
|
||||
if (player == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (session.isComplete()) {
|
||||
enterOverview(table, session, player);
|
||||
return;
|
||||
}
|
||||
|
||||
if (session.isAiVsAi()) {
|
||||
startAiTurn(table, session, player);
|
||||
return;
|
||||
}
|
||||
|
||||
MoveResult opponentMove = session.playOpponentTurn();
|
||||
if (!opponentMove.valid()) {
|
||||
player.displayClientMessage(Component.literal(opponentMove.errorMessage()).withStyle(ChatFormatting.RED), false);
|
||||
table.clearBoard();
|
||||
DuelSessionManager.end(player, true);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!table.setCard(opponentMove.playedCell().index(), CardItem.createCardStack(opponentMove.playedCardId(), TriadItems.TRIAD_CARD.get()), DuelTableBlockEntity.OWNER_SECOND)) {
|
||||
player.displayClientMessage(Component.literal("The Duel Table could not stage the opponent move.").withStyle(ChatFormatting.RED), false);
|
||||
table.clearBoard();
|
||||
DuelSessionManager.end(player, true);
|
||||
return;
|
||||
}
|
||||
|
||||
session.beginOpponentAnimation();
|
||||
table.updateHudState(session);
|
||||
table.startMoveAnimation(opponentMove, DuelTableBlockEntity.OWNER_SECOND);
|
||||
}
|
||||
|
||||
static void handleAfterOpponentAnimation(Level level, BlockPos pos, DuelTableBlockEntity table, DuelSession session) {
|
||||
ServerPlayer player = getPlayer(level, session);
|
||||
if (player == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (session.isComplete()) {
|
||||
enterOverview(table, session, player);
|
||||
return;
|
||||
}
|
||||
|
||||
if (session.isAiVsAi()) {
|
||||
startAiTurn(table, session, player);
|
||||
return;
|
||||
}
|
||||
|
||||
session.returnToPlaying();
|
||||
table.updateHudState(session);
|
||||
}
|
||||
|
||||
public static void startAiTurn(DuelTableBlockEntity table, DuelSession session, ServerPlayer player) {
|
||||
MoveResult aiMove = session.playAiTurn();
|
||||
if (!aiMove.valid()) {
|
||||
player.displayClientMessage(Component.literal(aiMove.errorMessage()).withStyle(ChatFormatting.RED), false);
|
||||
table.clearBoard();
|
||||
DuelSessionManager.end(player, true);
|
||||
return;
|
||||
}
|
||||
|
||||
int owner = session.isPlayerTurn() ? DuelTableBlockEntity.OWNER_SECOND : DuelTableBlockEntity.OWNER_FIRST;
|
||||
if (!table.setCard(aiMove.playedCell().index(), CardItem.createCardStack(aiMove.playedCardId(), TriadItems.TRIAD_CARD.get()), owner)) {
|
||||
player.displayClientMessage(Component.literal("The Duel Table could not stage the AI move.").withStyle(ChatFormatting.RED), false);
|
||||
table.clearBoard();
|
||||
DuelSessionManager.end(player, true);
|
||||
return;
|
||||
}
|
||||
|
||||
if (owner == DuelTableBlockEntity.OWNER_FIRST) {
|
||||
session.beginPlayerAnimation();
|
||||
} else {
|
||||
session.beginOpponentAnimation();
|
||||
}
|
||||
table.updateHudState(session);
|
||||
table.startMoveAnimation(aiMove, owner);
|
||||
}
|
||||
|
||||
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.updateHudState(session);
|
||||
table.startOverviewSweepAnimation();
|
||||
player.displayClientMessage(session.resultSummary().copy().withStyle(ChatFormatting.AQUA), false);
|
||||
}
|
||||
|
||||
private static ServerPlayer getPlayer(Level level, DuelSession session) {
|
||||
return level.getServer() == null ? null : level.getServer().getPlayerList().getPlayer(session.playerParticipantId());
|
||||
}
|
||||
}
|
||||
@@ -1,34 +1,81 @@
|
||||
package com.trunksbomb.minetriad.blockentity;
|
||||
|
||||
import java.util.ArrayDeque;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
import java.util.UUID;
|
||||
|
||||
import com.trunksbomb.minetriad.game.BoardCell;
|
||||
import com.trunksbomb.minetriad.game.DuelSession;
|
||||
import com.trunksbomb.minetriad.game.DuelSessionManager;
|
||||
import com.trunksbomb.minetriad.game.MoveResult;
|
||||
import com.trunksbomb.minetriad.registry.TriadBlockEntities;
|
||||
import com.trunksbomb.minetriad.registry.TriadItems;
|
||||
|
||||
import net.minecraft.ChatFormatting;
|
||||
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;
|
||||
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;
|
||||
public static final int OWNER_FIRST = 1;
|
||||
public static final int OWNER_SECOND = 2;
|
||||
private static final int PLACEMENT_DURATION = 10;
|
||||
private static final int CAPTURE_DURATION = 10;
|
||||
private static final int OVERVIEW_SWEEP_DURATION = 18;
|
||||
private static final int CLEAR_WAVE_DURATION = 10;
|
||||
private static final int REWARD_TAKE_DURATION = 14;
|
||||
|
||||
private final NonNullList<ItemStack> boardCards = NonNullList.withSize(SLOT_COUNT, ItemStack.EMPTY);
|
||||
private final NonNullList<ItemStack> rewardCards = NonNullList.withSize(REWARD_SLOT_COUNT, ItemStack.EMPTY);
|
||||
private final List<RewardTakeAnimation> rewardTakeAnimations = new ArrayList<>();
|
||||
private final int[] ownerSlots = new int[SLOT_COUNT];
|
||||
private final int[] slotAges = new int[SLOT_COUNT];
|
||||
private final ArrayDeque<AnimationStep> pendingAnimations = new ArrayDeque<>();
|
||||
private UUID firstParticipantId;
|
||||
private UUID secondParticipantId;
|
||||
private AnimationType animationType = AnimationType.NONE;
|
||||
private int[] animationSlots = new int[0];
|
||||
private int animationTargetOwner = OWNER_NONE;
|
||||
private int animationAge;
|
||||
private int animationDuration;
|
||||
private boolean animationOwnerApplied;
|
||||
private TableAnimationType tableAnimationType = TableAnimationType.NONE;
|
||||
private int tableAnimationAge;
|
||||
private int tableAnimationDuration;
|
||||
private boolean hudActive;
|
||||
private int hudP1HandCount;
|
||||
private int hudP2HandCount;
|
||||
private int hudP1Score;
|
||||
private int hudP2Score;
|
||||
private String hudRules = "";
|
||||
private String hudGameState = "";
|
||||
private String hudTurn = "";
|
||||
private String hudInstruction = "";
|
||||
|
||||
public DuelTableBlockEntity(BlockPos pos, BlockState blockState) {
|
||||
super(TriadBlockEntities.DUEL_TABLE.get(), pos, blockState);
|
||||
@@ -38,10 +85,32 @@ 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 List<RewardTakeAnimation> rewardTakeAnimations() {
|
||||
return List.copyOf(rewardTakeAnimations);
|
||||
}
|
||||
|
||||
public int ownerAt(int slot) {
|
||||
return ownerSlots[slot];
|
||||
}
|
||||
|
||||
public int slotAge(int slot) {
|
||||
return slot >= 0 && slot < slotAges.length ? slotAges[slot] : 0;
|
||||
}
|
||||
|
||||
public Optional<UUID> firstParticipantId() {
|
||||
return Optional.ofNullable(firstParticipantId);
|
||||
}
|
||||
@@ -50,12 +119,131 @@ public class DuelTableBlockEntity extends BlockEntity {
|
||||
return Optional.ofNullable(secondParticipantId);
|
||||
}
|
||||
|
||||
public boolean hudActive() {
|
||||
return hudActive;
|
||||
}
|
||||
|
||||
public int hudP1HandCount() {
|
||||
return hudP1HandCount;
|
||||
}
|
||||
|
||||
public int hudP2HandCount() {
|
||||
return hudP2HandCount;
|
||||
}
|
||||
|
||||
public int hudP1Score() {
|
||||
return hudP1Score;
|
||||
}
|
||||
|
||||
public int hudP2Score() {
|
||||
return hudP2Score;
|
||||
}
|
||||
|
||||
public String hudRules() {
|
||||
return hudRules;
|
||||
}
|
||||
|
||||
public String hudGameState() {
|
||||
return hudGameState;
|
||||
}
|
||||
|
||||
public String hudTurn() {
|
||||
return hudTurn;
|
||||
}
|
||||
|
||||
public String hudInstruction() {
|
||||
return hudInstruction;
|
||||
}
|
||||
|
||||
public boolean hasActiveAnimation() {
|
||||
return animationType != AnimationType.NONE || tableAnimationType != TableAnimationType.NONE;
|
||||
}
|
||||
|
||||
public String animationTypeName() {
|
||||
return animationType.name();
|
||||
}
|
||||
|
||||
public boolean isPlacementAnimation() {
|
||||
return animationType == AnimationType.PLACEMENT;
|
||||
}
|
||||
|
||||
public boolean isCaptureAnimation() {
|
||||
return animationType == AnimationType.CAPTURE;
|
||||
}
|
||||
|
||||
public int animationSlot() {
|
||||
return animationSlots.length > 0 ? animationSlots[0] : -1;
|
||||
}
|
||||
|
||||
public boolean isAnimatingSlot(int slot) {
|
||||
for (int animatedSlot : animationSlots) {
|
||||
if (animatedSlot == slot) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
public float animationProgress(float partialTick) {
|
||||
if (!hasActiveAnimation() || animationDuration <= 0) {
|
||||
return 0.0F;
|
||||
}
|
||||
return Math.clamp((animationAge + partialTick) / animationDuration, 0.0F, 1.0F);
|
||||
}
|
||||
|
||||
public boolean hasOverviewSweepAnimation() {
|
||||
return tableAnimationType == TableAnimationType.OVERVIEW_SWEEP;
|
||||
}
|
||||
|
||||
public boolean hasClearWaveAnimation() {
|
||||
return tableAnimationType == TableAnimationType.CLEARING;
|
||||
}
|
||||
|
||||
public float tableAnimationProgress(float partialTick) {
|
||||
if (tableAnimationType == TableAnimationType.NONE || tableAnimationDuration <= 0) {
|
||||
return 0.0F;
|
||||
}
|
||||
return Math.clamp((tableAnimationAge + partialTick) / tableAnimationDuration, 0.0F, 1.0F);
|
||||
}
|
||||
|
||||
public boolean animationOwnerApplied() {
|
||||
return animationOwnerApplied;
|
||||
}
|
||||
|
||||
public void setParticipants(UUID firstParticipantId, UUID secondParticipantId) {
|
||||
this.firstParticipantId = firstParticipantId;
|
||||
this.secondParticipantId = secondParticipantId;
|
||||
sync();
|
||||
}
|
||||
|
||||
public void updateHudState(DuelSession session) {
|
||||
hudActive = true;
|
||||
hudP1HandCount = session.playerHandCount();
|
||||
hudP2HandCount = session.opponentHandCount();
|
||||
hudP1Score = session.playerScore();
|
||||
hudP2Score = session.opponentScore();
|
||||
hudRules = session.rulesSummary();
|
||||
hudGameState = session.gameStateLabel();
|
||||
hudTurn = session.turnIndicator();
|
||||
hudInstruction = session.isInOverview()
|
||||
? rewardCardCount() > 0 ? "P1 choosing 1 cards to keep" : "Ready for next game"
|
||||
: session.nextStepInstruction();
|
||||
sync();
|
||||
}
|
||||
|
||||
public void clearHudState() {
|
||||
hudActive = false;
|
||||
hudP1HandCount = 0;
|
||||
hudP2HandCount = 0;
|
||||
hudP1Score = 0;
|
||||
hudP2Score = 0;
|
||||
hudRules = "";
|
||||
hudGameState = "";
|
||||
hudTurn = "";
|
||||
hudInstruction = "";
|
||||
sync();
|
||||
}
|
||||
|
||||
public boolean setCard(int slot, ItemStack stack, int owner) {
|
||||
if (slot < 0 || slot >= boardCards.size() || !boardCards.get(slot).isEmpty() || !stack.is(TriadItems.TRIAD_CARD.get())) {
|
||||
return false;
|
||||
@@ -63,6 +251,7 @@ public class DuelTableBlockEntity extends BlockEntity {
|
||||
|
||||
boardCards.set(slot, stack.copyWithCount(1));
|
||||
ownerSlots[slot] = owner;
|
||||
slotAges[slot] = 0;
|
||||
sync();
|
||||
return true;
|
||||
}
|
||||
@@ -79,12 +268,33 @@ public class DuelTableBlockEntity extends BlockEntity {
|
||||
for (int index = 0; index < boardCards.size(); index++) {
|
||||
boardCards.set(index, ItemStack.EMPTY);
|
||||
ownerSlots[index] = OWNER_NONE;
|
||||
slotAges[index] = 0;
|
||||
}
|
||||
pendingAnimations.clear();
|
||||
clearAnimationState();
|
||||
clearRewardCards();
|
||||
firstParticipantId = null;
|
||||
secondParticipantId = null;
|
||||
clearHudState();
|
||||
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);
|
||||
@@ -92,18 +302,267 @@ public class DuelTableBlockEntity extends BlockEntity {
|
||||
Block.popResource(level, pos, stack);
|
||||
boardCards.set(slot, ItemStack.EMPTY);
|
||||
ownerSlots[slot] = OWNER_NONE;
|
||||
slotAges[slot] = 0;
|
||||
}
|
||||
}
|
||||
pendingAnimations.clear();
|
||||
clearAnimationState();
|
||||
dropRewardCards(level, pos);
|
||||
firstParticipantId = null;
|
||||
secondParticipantId = null;
|
||||
clearHudState();
|
||||
sync();
|
||||
}
|
||||
|
||||
public void setRewardCards(List<ItemStack> 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));
|
||||
}
|
||||
}
|
||||
refreshRewardInteractions();
|
||||
DuelSession session = level == null ? null : DuelSessionManager.getAt(worldPosition);
|
||||
if (session != null) {
|
||||
updateHudState(session);
|
||||
}
|
||||
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;
|
||||
}
|
||||
int visibleIndex = visibleRewardIndex(slot);
|
||||
int rewardCountBeforeTake = rewardCardCount();
|
||||
if (visibleIndex >= 0 && rewardCountBeforeTake > 0) {
|
||||
BoardLocalSpace.SlotCenter center = BoardLocalSpace.rewardCenter(
|
||||
visibleIndex,
|
||||
getBlockState().getValue(DuelTableBlock.FACING),
|
||||
rewardCountBeforeTake);
|
||||
rewardTakeAnimations.add(new RewardTakeAnimation(taken.copyWithCount(1), center.x(), center.z(), 0, REWARD_TAKE_DURATION));
|
||||
}
|
||||
rewardCards.set(slot, ItemStack.EMPTY);
|
||||
refreshRewardInteractions();
|
||||
DuelSession session = level == null ? null : DuelSessionManager.getAt(worldPosition);
|
||||
if (session != null) {
|
||||
updateHudState(session);
|
||||
}
|
||||
sync();
|
||||
return taken;
|
||||
}
|
||||
|
||||
public void clearRewardCards() {
|
||||
for (int index = 0; index < rewardCards.size(); index++) {
|
||||
rewardCards.set(index, ItemStack.EMPTY);
|
||||
}
|
||||
rewardTakeAnimations.clear();
|
||||
removeRewardInteractions();
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
rewardTakeAnimations.clear();
|
||||
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) {
|
||||
pendingAnimations.clear();
|
||||
clearAnimationState();
|
||||
clearTableAnimationState();
|
||||
if (moveResult.playedCell() == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
pendingAnimations.addLast(new AnimationStep(AnimationType.PLACEMENT, new int[] {moveResult.playedCell().index()}, owner));
|
||||
for (List<BoardCell> captureWave : moveResult.captureWaves()) {
|
||||
int[] captureSlots = captureWave.stream()
|
||||
.mapToInt(BoardCell::index)
|
||||
.toArray();
|
||||
if (captureSlots.length > 0) {
|
||||
pendingAnimations.addLast(new AnimationStep(AnimationType.CAPTURE, captureSlots, owner));
|
||||
}
|
||||
}
|
||||
advanceAnimationStep();
|
||||
}
|
||||
|
||||
public void startOverviewSweepAnimation() {
|
||||
clearAnimationState();
|
||||
tableAnimationType = TableAnimationType.OVERVIEW_SWEEP;
|
||||
tableAnimationAge = 0;
|
||||
tableAnimationDuration = OVERVIEW_SWEEP_DURATION;
|
||||
sync();
|
||||
}
|
||||
|
||||
public void startClearWaveAnimation() {
|
||||
clearAnimationState();
|
||||
tableAnimationType = TableAnimationType.CLEARING;
|
||||
tableAnimationAge = 0;
|
||||
tableAnimationDuration = CLEAR_WAVE_DURATION;
|
||||
sync();
|
||||
}
|
||||
|
||||
public static void tick(Level level, BlockPos pos, BlockState state, DuelTableBlockEntity blockEntity) {
|
||||
if (blockEntity.hasActiveAnimation()) {
|
||||
if (blockEntity.animationType != AnimationType.NONE) {
|
||||
blockEntity.animationAge++;
|
||||
if (!level.isClientSide
|
||||
&& blockEntity.animationType == AnimationType.CAPTURE
|
||||
&& !blockEntity.animationOwnerApplied
|
||||
&& blockEntity.animationAge >= Math.max(1, blockEntity.animationDuration / 2)) {
|
||||
for (int slot : blockEntity.animationSlots) {
|
||||
blockEntity.setOwner(slot, blockEntity.animationTargetOwner);
|
||||
}
|
||||
blockEntity.animationOwnerApplied = true;
|
||||
blockEntity.sync();
|
||||
}
|
||||
|
||||
if (blockEntity.animationAge >= blockEntity.animationDuration) {
|
||||
if (!level.isClientSide) {
|
||||
blockEntity.advanceAnimationStep();
|
||||
if (blockEntity.animationType == AnimationType.NONE && blockEntity.tableAnimationType == TableAnimationType.NONE) {
|
||||
blockEntity.resolvePostAnimation(level, pos);
|
||||
}
|
||||
} else {
|
||||
blockEntity.clearAnimationState();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (blockEntity.tableAnimationType != TableAnimationType.NONE) {
|
||||
blockEntity.tableAnimationAge++;
|
||||
if (!level.isClientSide
|
||||
&& blockEntity.tableAnimationType == TableAnimationType.CLEARING) {
|
||||
blockEntity.updateClearWaveState(level, pos);
|
||||
}
|
||||
if (blockEntity.tableAnimationAge >= blockEntity.tableAnimationDuration) {
|
||||
if (!level.isClientSide) {
|
||||
blockEntity.clearTableAnimationState();
|
||||
blockEntity.resolvePostAnimation(level, pos);
|
||||
} else {
|
||||
blockEntity.clearTableAnimationState();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for (int slot = 0; slot < SLOT_COUNT; slot++) {
|
||||
if (!blockEntity.boardCards.get(slot).isEmpty()) {
|
||||
blockEntity.slotAges[slot]++;
|
||||
} else {
|
||||
blockEntity.slotAges[slot] = 0;
|
||||
}
|
||||
}
|
||||
|
||||
if (!blockEntity.rewardTakeAnimations.isEmpty()) {
|
||||
boolean changed = false;
|
||||
for (int index = blockEntity.rewardTakeAnimations.size() - 1; index >= 0; index--) {
|
||||
RewardTakeAnimation animation = blockEntity.rewardTakeAnimations.get(index);
|
||||
animation.age++;
|
||||
if (animation.age >= animation.duration) {
|
||||
blockEntity.rewardTakeAnimations.remove(index);
|
||||
changed = true;
|
||||
}
|
||||
}
|
||||
if (!level.isClientSide && changed) {
|
||||
blockEntity.sync();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
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);
|
||||
saveRewardTakeAnimations(tag, registries);
|
||||
saveHudState(tag);
|
||||
tag.putIntArray("OwnerSlots", ownerSlots);
|
||||
tag.putIntArray("SlotAges", slotAges);
|
||||
saveAnimation(tag);
|
||||
saveTableAnimation(tag);
|
||||
if (firstParticipantId != null) {
|
||||
tag.putUUID("FirstParticipantId", firstParticipantId);
|
||||
}
|
||||
@@ -117,14 +576,29 @@ 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);
|
||||
}
|
||||
loadRewardTakeAnimations(tag, registries);
|
||||
loadHudState(tag);
|
||||
loadOwnerData(tag);
|
||||
loadAnimation(tag);
|
||||
loadTableAnimation(tag);
|
||||
}
|
||||
|
||||
@Override
|
||||
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);
|
||||
saveRewardTakeAnimations(tag, registries);
|
||||
saveHudState(tag);
|
||||
tag.putIntArray("OwnerSlots", ownerSlots);
|
||||
tag.putIntArray("SlotAges", slotAges);
|
||||
saveAnimation(tag);
|
||||
saveTableAnimation(tag);
|
||||
if (firstParticipantId != null) {
|
||||
tag.putUUID("FirstParticipantId", firstParticipantId);
|
||||
}
|
||||
@@ -145,7 +619,14 @@ 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);
|
||||
}
|
||||
loadRewardTakeAnimations(tag, registries);
|
||||
loadHudState(tag);
|
||||
loadOwnerData(tag);
|
||||
loadAnimation(tag);
|
||||
loadTableAnimation(tag);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -153,9 +634,22 @@ public class DuelTableBlockEntity extends BlockEntity {
|
||||
for (int index = 0; index < boardCards.size(); index++) {
|
||||
boardCards.set(index, ItemStack.EMPTY);
|
||||
ownerSlots[index] = OWNER_NONE;
|
||||
slotAges[index] = 0;
|
||||
}
|
||||
pendingAnimations.clear();
|
||||
clearAnimationState();
|
||||
clearRewardCards();
|
||||
firstParticipantId = null;
|
||||
secondParticipantId = null;
|
||||
hudActive = false;
|
||||
hudP1HandCount = 0;
|
||||
hudP2HandCount = 0;
|
||||
hudP1Score = 0;
|
||||
hudP2Score = 0;
|
||||
hudRules = "";
|
||||
hudGameState = "";
|
||||
hudTurn = "";
|
||||
hudInstruction = "";
|
||||
}
|
||||
|
||||
private void loadOwnerData(CompoundTag tag) {
|
||||
@@ -163,14 +657,265 @@ public class DuelTableBlockEntity extends BlockEntity {
|
||||
for (int index = 0; index < ownerSlots.length; index++) {
|
||||
ownerSlots[index] = index < loadedOwners.length ? loadedOwners[index] : OWNER_NONE;
|
||||
}
|
||||
int[] loadedAges = tag.getIntArray("SlotAges");
|
||||
for (int index = 0; index < slotAges.length; index++) {
|
||||
slotAges[index] = index < loadedAges.length ? loadedAges[index] : 0;
|
||||
}
|
||||
firstParticipantId = tag.hasUUID("FirstParticipantId") ? tag.getUUID("FirstParticipantId") : null;
|
||||
secondParticipantId = tag.hasUUID("SecondParticipantId") ? tag.getUUID("SecondParticipantId") : null;
|
||||
}
|
||||
|
||||
private void saveRewardTakeAnimations(CompoundTag tag, HolderLookup.Provider registries) {
|
||||
CompoundTag animationsTag = new CompoundTag();
|
||||
for (int index = 0; index < rewardTakeAnimations.size(); index++) {
|
||||
RewardTakeAnimation animation = rewardTakeAnimations.get(index);
|
||||
CompoundTag animationTag = new CompoundTag();
|
||||
animationTag.put("Stack", animation.stack.saveOptional(registries));
|
||||
animationTag.putFloat("LocalX", animation.localX);
|
||||
animationTag.putFloat("LocalZ", animation.localZ);
|
||||
animationTag.putInt("Age", animation.age);
|
||||
animationTag.putInt("Duration", animation.duration);
|
||||
animationsTag.put("Animation" + index, animationTag);
|
||||
}
|
||||
animationsTag.putInt("Count", rewardTakeAnimations.size());
|
||||
tag.put("RewardTakeAnimations", animationsTag);
|
||||
}
|
||||
|
||||
private void saveHudState(CompoundTag tag) {
|
||||
tag.putBoolean("HudActive", hudActive);
|
||||
tag.putInt("HudP1HandCount", hudP1HandCount);
|
||||
tag.putInt("HudP2HandCount", hudP2HandCount);
|
||||
tag.putInt("HudP1Score", hudP1Score);
|
||||
tag.putInt("HudP2Score", hudP2Score);
|
||||
tag.putString("HudRules", hudRules);
|
||||
tag.putString("HudGameState", hudGameState);
|
||||
tag.putString("HudTurn", hudTurn);
|
||||
tag.putString("HudInstruction", hudInstruction);
|
||||
}
|
||||
|
||||
private void loadHudState(CompoundTag tag) {
|
||||
hudActive = tag.getBoolean("HudActive");
|
||||
hudP1HandCount = tag.getInt("HudP1HandCount");
|
||||
hudP2HandCount = tag.getInt("HudP2HandCount");
|
||||
hudP1Score = tag.getInt("HudP1Score");
|
||||
hudP2Score = tag.getInt("HudP2Score");
|
||||
hudRules = tag.getString("HudRules");
|
||||
hudGameState = tag.getString("HudGameState");
|
||||
hudTurn = tag.getString("HudTurn");
|
||||
hudInstruction = tag.getString("HudInstruction");
|
||||
}
|
||||
|
||||
private void loadRewardTakeAnimations(CompoundTag tag, HolderLookup.Provider registries) {
|
||||
rewardTakeAnimations.clear();
|
||||
if (!tag.contains("RewardTakeAnimations", Tag.TAG_COMPOUND)) {
|
||||
return;
|
||||
}
|
||||
CompoundTag animationsTag = tag.getCompound("RewardTakeAnimations");
|
||||
int count = animationsTag.getInt("Count");
|
||||
for (int index = 0; index < count; index++) {
|
||||
CompoundTag animationTag = animationsTag.getCompound("Animation" + index);
|
||||
ItemStack stack = ItemStack.parseOptional(registries, animationTag.getCompound("Stack"));
|
||||
if (stack.isEmpty()) {
|
||||
continue;
|
||||
}
|
||||
rewardTakeAnimations.add(new RewardTakeAnimation(
|
||||
stack,
|
||||
animationTag.getFloat("LocalX"),
|
||||
animationTag.getFloat("LocalZ"),
|
||||
animationTag.getInt("Age"),
|
||||
animationTag.getInt("Duration")));
|
||||
}
|
||||
}
|
||||
|
||||
private void saveAnimation(CompoundTag tag) {
|
||||
tag.putString("AnimationType", animationType.name());
|
||||
tag.putIntArray("AnimationSlots", animationSlots);
|
||||
tag.putInt("AnimationTargetOwner", animationTargetOwner);
|
||||
tag.putInt("AnimationAge", animationAge);
|
||||
tag.putInt("AnimationDuration", animationDuration);
|
||||
tag.putBoolean("AnimationOwnerApplied", animationOwnerApplied);
|
||||
}
|
||||
|
||||
private void loadAnimation(CompoundTag tag) {
|
||||
animationType = AnimationType.valueOf(tag.getString("AnimationType").isEmpty() ? AnimationType.NONE.name() : tag.getString("AnimationType"));
|
||||
animationSlots = tag.getIntArray("AnimationSlots");
|
||||
animationTargetOwner = tag.getInt("AnimationTargetOwner");
|
||||
animationAge = tag.getInt("AnimationAge");
|
||||
animationDuration = tag.getInt("AnimationDuration");
|
||||
animationOwnerApplied = tag.getBoolean("AnimationOwnerApplied");
|
||||
}
|
||||
|
||||
private void saveTableAnimation(CompoundTag tag) {
|
||||
tag.putString("TableAnimationType", tableAnimationType.name());
|
||||
tag.putInt("TableAnimationAge", tableAnimationAge);
|
||||
tag.putInt("TableAnimationDuration", tableAnimationDuration);
|
||||
}
|
||||
|
||||
private void loadTableAnimation(CompoundTag tag) {
|
||||
tableAnimationType = TableAnimationType.valueOf(tag.getString("TableAnimationType").isEmpty() ? TableAnimationType.NONE.name() : tag.getString("TableAnimationType"));
|
||||
tableAnimationAge = tag.getInt("TableAnimationAge");
|
||||
tableAnimationDuration = tag.getInt("TableAnimationDuration");
|
||||
}
|
||||
|
||||
private void advanceAnimationStep() {
|
||||
AnimationStep nextStep = pendingAnimations.pollFirst();
|
||||
if (nextStep == null) {
|
||||
clearAnimationState();
|
||||
sync();
|
||||
return;
|
||||
}
|
||||
|
||||
animationType = nextStep.type();
|
||||
animationSlots = nextStep.slots();
|
||||
animationTargetOwner = nextStep.targetOwner();
|
||||
animationAge = 0;
|
||||
animationDuration = nextStep.type() == AnimationType.PLACEMENT ? PLACEMENT_DURATION : CAPTURE_DURATION;
|
||||
animationOwnerApplied = nextStep.type() != AnimationType.CAPTURE;
|
||||
sync();
|
||||
}
|
||||
|
||||
private void clearAnimationState() {
|
||||
animationType = AnimationType.NONE;
|
||||
animationSlots = new int[0];
|
||||
animationTargetOwner = OWNER_NONE;
|
||||
animationAge = 0;
|
||||
animationDuration = 0;
|
||||
animationOwnerApplied = false;
|
||||
}
|
||||
|
||||
private void clearTableAnimationState() {
|
||||
tableAnimationType = TableAnimationType.NONE;
|
||||
tableAnimationAge = 0;
|
||||
tableAnimationDuration = 0;
|
||||
}
|
||||
|
||||
private void resolvePostAnimation(Level level, BlockPos pos) {
|
||||
DuelSession session = DuelSessionManager.getAt(pos);
|
||||
if (session == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (session.isClosingAnimation()) {
|
||||
clearBoard();
|
||||
var player = level.getServer() == null ? null : level.getServer().getPlayerList().getPlayer(session.playerParticipantId());
|
||||
if (player != null) {
|
||||
DuelSessionManager.end(player, true);
|
||||
player.displayClientMessage(Component.literal("Duel finished. All played cards have been returned.").withStyle(ChatFormatting.GREEN), false);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (session.isAwaitingPlayerAnimation()) {
|
||||
DuelTableAnimationResolver.handleAfterPlayerAnimation(level, pos, this, session);
|
||||
return;
|
||||
}
|
||||
|
||||
if (session.isAwaitingOpponentAnimation()) {
|
||||
DuelTableAnimationResolver.handleAfterOpponentAnimation(level, pos, this, session);
|
||||
}
|
||||
}
|
||||
|
||||
private void updateClearWaveState(Level level, BlockPos pos) {
|
||||
for (int slot = 0; slot < SLOT_COUNT; slot++) {
|
||||
if (boardCards.get(slot).isEmpty()) {
|
||||
continue;
|
||||
}
|
||||
if (clearWaveSlotProgress(slot, 0.0F) >= 1.0F) {
|
||||
boardCards.set(slot, ItemStack.EMPTY);
|
||||
ownerSlots[slot] = OWNER_NONE;
|
||||
slotAges[slot] = 0;
|
||||
}
|
||||
}
|
||||
sync();
|
||||
}
|
||||
|
||||
public float overviewSweepSlotProgress(int slot, float partialTick) {
|
||||
return waveSlotProgress(slot, partialTick, SLOT_COUNT, tableAnimationAge, tableAnimationDuration);
|
||||
}
|
||||
|
||||
public float clearWaveSlotProgress(int slot, float partialTick) {
|
||||
return waveSlotProgress(slot, partialTick, SLOT_COUNT, tableAnimationAge, tableAnimationDuration);
|
||||
}
|
||||
|
||||
private float waveSlotProgress(int slot, float partialTick, int slotCount, int age, int duration) {
|
||||
if (slot < 0 || slot >= slotCount || duration <= 0) {
|
||||
return 0.0F;
|
||||
}
|
||||
float global = Math.clamp((age + partialTick) / duration, 0.0F, 1.0F);
|
||||
float delay = slot / (float) slotCount;
|
||||
float window = 0.28F;
|
||||
return Math.clamp((global - delay) / window, 0.0F, 1.0F);
|
||||
}
|
||||
|
||||
private void sync() {
|
||||
setChanged();
|
||||
if (level != null) {
|
||||
level.sendBlockUpdated(worldPosition, getBlockState(), getBlockState(), Block.UPDATE_ALL);
|
||||
}
|
||||
}
|
||||
|
||||
private enum AnimationType {
|
||||
NONE,
|
||||
PLACEMENT,
|
||||
CAPTURE
|
||||
}
|
||||
|
||||
private enum TableAnimationType {
|
||||
NONE,
|
||||
OVERVIEW_SWEEP,
|
||||
CLEARING
|
||||
}
|
||||
|
||||
private record AnimationStep(AnimationType type, int[] slots, int targetOwner) {
|
||||
}
|
||||
|
||||
public static final class RewardTakeAnimation {
|
||||
private final ItemStack stack;
|
||||
private final float localX;
|
||||
private final float localZ;
|
||||
private int age;
|
||||
private final int duration;
|
||||
|
||||
private RewardTakeAnimation(ItemStack stack, float localX, float localZ, int age, int duration) {
|
||||
this.stack = stack;
|
||||
this.localX = localX;
|
||||
this.localZ = localZ;
|
||||
this.age = age;
|
||||
this.duration = duration;
|
||||
}
|
||||
|
||||
public ItemStack stack() {
|
||||
return stack;
|
||||
}
|
||||
|
||||
public float localX() {
|
||||
return localX;
|
||||
}
|
||||
|
||||
public float localZ() {
|
||||
return localZ;
|
||||
}
|
||||
|
||||
public int age() {
|
||||
return age;
|
||||
}
|
||||
|
||||
public int duration() {
|
||||
return duration;
|
||||
}
|
||||
}
|
||||
|
||||
private int visibleRewardIndex(int slot) {
|
||||
int visibleIndex = 0;
|
||||
for (int index = 0; index < rewardCards.size(); index++) {
|
||||
if (rewardCards.get(index).isEmpty()) {
|
||||
continue;
|
||||
}
|
||||
if (index == slot) {
|
||||
return visibleIndex;
|
||||
}
|
||||
visibleIndex++;
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,130 @@
|
||||
package com.trunksbomb.minetriad.client.hud;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
import com.trunksbomb.minetriad.blockentity.DuelTableBlockEntity;
|
||||
|
||||
import net.minecraft.client.Minecraft;
|
||||
import net.minecraft.client.gui.GuiGraphics;
|
||||
import net.minecraft.core.BlockPos;
|
||||
import net.minecraft.network.chat.Component;
|
||||
import net.minecraft.world.level.block.entity.BlockEntity;
|
||||
import net.neoforged.neoforge.client.event.ClientTickEvent;
|
||||
import net.neoforged.neoforge.client.event.RenderGuiLayerEvent;
|
||||
|
||||
public final class LocalDuelHudOverlay {
|
||||
private static final int SCAN_INTERVAL_TICKS = 10;
|
||||
private static final int STALE_TIMEOUT_TICKS = 20;
|
||||
private static final double MAX_DISTANCE_SQ = 10.0D * 10.0D;
|
||||
|
||||
private static BlockPos trackedTablePos;
|
||||
private static int nextScanIn;
|
||||
private static int staleTicksRemaining;
|
||||
|
||||
private LocalDuelHudOverlay() {
|
||||
}
|
||||
|
||||
public static void onClientTick(ClientTickEvent.Post event) {
|
||||
Minecraft minecraft = Minecraft.getInstance();
|
||||
if (minecraft.level == null || minecraft.player == null) {
|
||||
clear();
|
||||
return;
|
||||
}
|
||||
|
||||
if (trackedTablePos != null && !isValidTrackedTable(minecraft, trackedTablePos)) {
|
||||
clear();
|
||||
}
|
||||
|
||||
if (nextScanIn > 0) {
|
||||
nextScanIn--;
|
||||
} else {
|
||||
trackedTablePos = findNearestActiveTable(minecraft);
|
||||
nextScanIn = SCAN_INTERVAL_TICKS;
|
||||
staleTicksRemaining = trackedTablePos == null ? 0 : STALE_TIMEOUT_TICKS;
|
||||
}
|
||||
|
||||
if (trackedTablePos != null) {
|
||||
staleTicksRemaining--;
|
||||
if (staleTicksRemaining <= 0 && !isValidTrackedTable(minecraft, trackedTablePos)) {
|
||||
clear();
|
||||
} else if (isValidTrackedTable(minecraft, trackedTablePos)) {
|
||||
staleTicksRemaining = STALE_TIMEOUT_TICKS;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public static void onRenderGui(RenderGuiLayerEvent.Post event) {
|
||||
Minecraft minecraft = Minecraft.getInstance();
|
||||
if (minecraft.level == null || minecraft.player == null || trackedTablePos == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
DuelTableBlockEntity table = duelTableAt(minecraft, trackedTablePos);
|
||||
if (table == null || !table.hudActive()) {
|
||||
return;
|
||||
}
|
||||
|
||||
List<Component> lines = new ArrayList<>();
|
||||
lines.add(Component.literal("P1 Cards in hand: " + table.hudP1HandCount()));
|
||||
lines.add(Component.literal("P2 Cards in hand: " + table.hudP2HandCount()));
|
||||
lines.add(Component.literal("Score: " + table.hudP1Score() + ":" + table.hudP2Score()));
|
||||
lines.add(Component.literal("Rules: " + table.hudRules()));
|
||||
lines.add(Component.literal("Game state: " + table.hudGameState()));
|
||||
lines.add(Component.literal("Turn: " + table.hudTurn()));
|
||||
lines.add(Component.literal("Next: " + table.hudInstruction()));
|
||||
|
||||
GuiGraphics guiGraphics = event.getGuiGraphics();
|
||||
int x = 8;
|
||||
int y = 8;
|
||||
int lineHeight = 10;
|
||||
int width = 0;
|
||||
for (Component line : lines) {
|
||||
width = Math.max(width, minecraft.font.width(line));
|
||||
}
|
||||
int height = lines.size() * lineHeight + 8;
|
||||
guiGraphics.fill(x - 4, y - 4, x + width + 4, y + height - 4, 0x80000000);
|
||||
for (int index = 0; index < lines.size(); index++) {
|
||||
guiGraphics.drawString(minecraft.font, lines.get(index), x, y + index * lineHeight, 0xFFFFFF, false);
|
||||
}
|
||||
}
|
||||
|
||||
private static BlockPos findNearestActiveTable(Minecraft minecraft) {
|
||||
BlockPos origin = minecraft.player.blockPosition();
|
||||
BlockPos nearest = null;
|
||||
double nearestDistance = MAX_DISTANCE_SQ;
|
||||
for (BlockPos pos : BlockPos.betweenClosed(origin.offset(-8, -4, -8), origin.offset(8, 4, 8))) {
|
||||
DuelTableBlockEntity table = duelTableAt(minecraft, pos);
|
||||
if (table == null || !table.hudActive()) {
|
||||
continue;
|
||||
}
|
||||
double distance = pos.distSqr(minecraft.player.blockPosition());
|
||||
if (distance <= nearestDistance) {
|
||||
nearestDistance = distance;
|
||||
nearest = pos.immutable();
|
||||
}
|
||||
}
|
||||
return nearest;
|
||||
}
|
||||
|
||||
private static boolean isValidTrackedTable(Minecraft minecraft, BlockPos pos) {
|
||||
DuelTableBlockEntity table = duelTableAt(minecraft, pos);
|
||||
return table != null
|
||||
&& table.hudActive()
|
||||
&& pos.distSqr(minecraft.player.blockPosition()) <= MAX_DISTANCE_SQ;
|
||||
}
|
||||
|
||||
private static DuelTableBlockEntity duelTableAt(Minecraft minecraft, BlockPos pos) {
|
||||
if (minecraft.level == null) {
|
||||
return null;
|
||||
}
|
||||
BlockEntity blockEntity = minecraft.level.getBlockEntity(pos);
|
||||
return blockEntity instanceof DuelTableBlockEntity duelTableBlockEntity ? duelTableBlockEntity : null;
|
||||
}
|
||||
|
||||
private static void clear() {
|
||||
trackedTablePos = null;
|
||||
nextScanIn = 0;
|
||||
staleTicksRemaining = 0;
|
||||
}
|
||||
}
|
||||
@@ -8,12 +8,21 @@ 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;
|
||||
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;
|
||||
|
||||
public DuelTableBlockEntityRenderer(BlockEntityRendererProvider.Context context) {
|
||||
}
|
||||
|
||||
@@ -27,15 +36,155 @@ public class DuelTableBlockEntityRenderer implements BlockEntityRenderer<DuelTab
|
||||
|
||||
BoardCell cell = new BoardCell(slot / BoardCell.SIZE, slot % BoardCell.SIZE);
|
||||
BoardLocalSpace.SlotCenter center = BoardLocalSpace.slotCenter(cell, blockEntity.getBlockState().getValue(DuelTableBlock.FACING));
|
||||
float y = TABLE_CARD_Y;
|
||||
float extraFlip = 0.0F;
|
||||
boolean hideCard = false;
|
||||
float idleOffset = idleBob(blockEntity, slot, partialTick);
|
||||
if (blockEntity.isAnimatingSlot(slot)) {
|
||||
float progress = blockEntity.animationProgress(partialTick);
|
||||
if (blockEntity.isPlacementAnimation()) {
|
||||
y += (1.0F - progress) * 0.18F;
|
||||
} else if (blockEntity.isCaptureAnimation()) {
|
||||
y += idleOffset;
|
||||
y += (float) Math.sin(progress * Math.PI) * 0.08F;
|
||||
extraFlip = 360.0F * progress;
|
||||
}
|
||||
} else {
|
||||
y += idleOffset;
|
||||
}
|
||||
if (blockEntity.hasOverviewSweepAnimation()) {
|
||||
float progress = blockEntity.overviewSweepSlotProgress(slot, partialTick);
|
||||
y += (float) Math.sin(progress * Math.PI) * 0.05F;
|
||||
}
|
||||
if (blockEntity.hasClearWaveAnimation()) {
|
||||
float progress = blockEntity.clearWaveSlotProgress(slot, partialTick);
|
||||
y += progress * 0.22F;
|
||||
hideCard = progress >= 1.0F;
|
||||
}
|
||||
if (hideCard) {
|
||||
continue;
|
||||
}
|
||||
|
||||
poseStack.pushPose();
|
||||
poseStack.translate(center.x(), 1.066F, center.z());
|
||||
poseStack.translate(center.x(), y, center.z());
|
||||
poseStack.mulPose(Axis.YP.rotationDegrees(BoardLocalSpace.cardYawDegrees(blockEntity.getBlockState().getValue(DuelTableBlock.FACING))));
|
||||
if (extraFlip != 0.0F) {
|
||||
poseStack.mulPose(Axis.ZP.rotationDegrees(extraFlip));
|
||||
}
|
||||
poseStack.mulPose(Axis.XN.rotationDegrees(90.0F));
|
||||
poseStack.scale(0.24F, 0.24F, 0.24F);
|
||||
TriadCardItemRenderer.renderCard(stack, poseStack, buffer, perspectivePalette(blockEntity, blockEntity.ownerAt(slot)));
|
||||
poseStack.popPose();
|
||||
}
|
||||
|
||||
renderRewardCards(blockEntity, partialTick, poseStack, buffer);
|
||||
renderRewardTakeAnimations(blockEntity, partialTick, poseStack, buffer);
|
||||
renderRewardProxyDebug(blockEntity, poseStack, buffer);
|
||||
}
|
||||
|
||||
private static void renderRewardCards(DuelTableBlockEntity blockEntity, float partialTick, PoseStack poseStack, MultiBufferSource buffer) {
|
||||
int rewardCount = blockEntity.rewardCardCount();
|
||||
if (rewardCount <= 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
int rewardIndex = 0;
|
||||
for (int slot = 0; slot < DuelTableBlockEntity.REWARD_SLOT_COUNT; slot++) {
|
||||
ItemStack rewardStack = blockEntity.getRewardCard(slot);
|
||||
if (rewardStack.isEmpty()) {
|
||||
continue;
|
||||
}
|
||||
|
||||
BoardLocalSpace.SlotCenter center = BoardLocalSpace.rewardCenter(
|
||||
rewardIndex,
|
||||
blockEntity.getBlockState().getValue(DuelTableBlock.FACING),
|
||||
rewardCount);
|
||||
float manifestProgress = blockEntity.hasOverviewSweepAnimation()
|
||||
? Math.max(0.2F, blockEntity.overviewSweepSlotProgress(Math.min(slot, DuelTableBlockEntity.SLOT_COUNT - 1), partialTick))
|
||||
: 1.0F;
|
||||
float y = 1.0F + (REWARD_CARD_Y - 1.0F) * manifestProgress + (float) Math.sin((Minecraft.getInstance().level == null ? 0.0F : (Minecraft.getInstance().level.getGameTime() + partialTick) * 0.08F) + rewardIndex * 0.7F) * 0.01F;
|
||||
|
||||
poseStack.pushPose();
|
||||
poseStack.translate(center.x(), y, center.z());
|
||||
poseStack.mulPose(Axis.YP.rotationDegrees(blockEntity.getBlockState().getValue(DuelTableBlock.FACING).toYRot()));
|
||||
poseStack.scale(0.3F, 0.3F, 0.3F);
|
||||
TriadCardItemRenderer.renderCard(rewardStack, poseStack, buffer, perspectivePalette(blockEntity, DuelTableBlockEntity.OWNER_SECOND));
|
||||
poseStack.popPose();
|
||||
rewardIndex++;
|
||||
}
|
||||
}
|
||||
|
||||
private static float idleBob(DuelTableBlockEntity blockEntity, int slot, float partialTick) {
|
||||
int age = blockEntity.slotAge(slot);
|
||||
int delay = 6 + deterministicOffset(blockEntity, slot) % 18;
|
||||
if (age <= delay) {
|
||||
return 0.0F;
|
||||
}
|
||||
|
||||
float bobTime = (age - delay) + partialTick;
|
||||
return (float) Math.sin(bobTime * IDLE_BOB_SPEED) * IDLE_BOB_HEIGHT;
|
||||
}
|
||||
|
||||
private static int deterministicOffset(DuelTableBlockEntity blockEntity, int slot) {
|
||||
return Math.floorMod(blockEntity.getBlockPos().hashCode() * 31 + slot * 17, 97);
|
||||
}
|
||||
|
||||
private static void renderRewardTakeAnimations(DuelTableBlockEntity blockEntity, float partialTick, PoseStack poseStack, MultiBufferSource buffer) {
|
||||
var facing = blockEntity.getBlockState().getValue(DuelTableBlock.FACING);
|
||||
float forwardX = facing.getStepX() * 0.33F;
|
||||
float forwardZ = facing.getStepZ() * 0.33F;
|
||||
for (DuelTableBlockEntity.RewardTakeAnimation animation : blockEntity.rewardTakeAnimations()) {
|
||||
float progress = Math.clamp((animation.age() + partialTick) / animation.duration(), 0.0F, 1.0F);
|
||||
float riseProgress = Math.min(1.0F, progress / 0.45F);
|
||||
float arcProgress = progress <= 0.45F ? 0.0F : (progress - 0.45F) / 0.55F;
|
||||
|
||||
float x = animation.localX() + forwardX * arcProgress;
|
||||
float y = REWARD_CARD_Y
|
||||
+ riseProgress * 0.26F
|
||||
+ (float) Math.sin(arcProgress * Math.PI) * 0.08F
|
||||
- arcProgress * 0.26F;
|
||||
float z = animation.localZ() + forwardZ * arcProgress;
|
||||
float spin = progress * 720.0F;
|
||||
|
||||
poseStack.pushPose();
|
||||
poseStack.translate(x, y, z);
|
||||
poseStack.mulPose(Axis.YP.rotationDegrees(facing.toYRot()));
|
||||
poseStack.mulPose(Axis.YP.rotationDegrees(spin));
|
||||
poseStack.scale(0.3F, 0.3F, 0.3F);
|
||||
TriadCardItemRenderer.renderCard(animation.stack(), poseStack, buffer, perspectivePalette(blockEntity, DuelTableBlockEntity.OWNER_SECOND));
|
||||
poseStack.popPose();
|
||||
}
|
||||
}
|
||||
|
||||
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) {
|
||||
|
||||
@@ -27,12 +27,15 @@ import org.joml.Matrix4f;
|
||||
public final class TriadCardItemRenderer extends BlockEntityWithoutLevelRenderer {
|
||||
private static final float CARD_MIN = -0.42F;
|
||||
private static final float CARD_MAX = 0.42F;
|
||||
private static final float FRONT_Z = 0.024F;
|
||||
private static final float BACK_Z = -0.024F;
|
||||
private static final float BORDER_Z = 0.026F;
|
||||
private static final float VALUE_Z = 0.030F;
|
||||
private static final float CARD_DEPTH = 0.024F;
|
||||
private static final float FRONT_Z = CARD_DEPTH;
|
||||
private static final float BACK_Z = -CARD_DEPTH;
|
||||
private static final float ART_Z = FRONT_Z + 0.004F;
|
||||
private static final float BORDER_Z = FRONT_Z + 0.008F;
|
||||
private static final float VALUE_Z = FRONT_Z + 0.012F;
|
||||
private static final float INNER_MIN = -0.37F;
|
||||
private static final float INNER_MAX = 0.37F;
|
||||
private static final float[] EDGE_COLOR = new float[] {0.18F, 0.14F, 0.10F};
|
||||
private static TriadCardItemRenderer INSTANCE;
|
||||
private static final CardPalette NEUTRAL_PALETTE = new CardPalette(
|
||||
new float[] {0.97F, 0.93F, 0.80F},
|
||||
@@ -77,6 +80,7 @@ public final class TriadCardItemRenderer extends BlockEntityWithoutLevelRenderer
|
||||
CardDefinition card = cardData == null || cardData.equals(CardStackData.EMPTY) ? null : CardRegistry.get(cardData.cardId());
|
||||
|
||||
poseStack.pushPose();
|
||||
drawCardBody(poseStack, buffer);
|
||||
drawBack(poseStack, buffer);
|
||||
if (card != null) {
|
||||
drawFrontArt(poseStack, buffer, artTexture(card));
|
||||
@@ -94,6 +98,25 @@ public final class TriadCardItemRenderer extends BlockEntityWithoutLevelRenderer
|
||||
poseStack.popPose();
|
||||
}
|
||||
|
||||
private static void drawCardBody(PoseStack poseStack, MultiBufferSource buffer) {
|
||||
float r = EDGE_COLOR[0];
|
||||
float g = EDGE_COLOR[1];
|
||||
float b = EDGE_COLOR[2];
|
||||
LevelRenderer.addChainedFilledBoxVertices(
|
||||
poseStack,
|
||||
buffer.getBuffer(RenderType.debugFilledBox()),
|
||||
CARD_MIN,
|
||||
CARD_MIN,
|
||||
BACK_Z,
|
||||
CARD_MAX,
|
||||
CARD_MAX,
|
||||
FRONT_Z,
|
||||
r,
|
||||
g,
|
||||
b,
|
||||
1.0F);
|
||||
}
|
||||
|
||||
private static String displayRank(int rank) {
|
||||
return rank == 10 ? "A" : Integer.toString(rank);
|
||||
}
|
||||
@@ -161,7 +184,7 @@ public final class TriadCardItemRenderer extends BlockEntityWithoutLevelRenderer
|
||||
}
|
||||
|
||||
private static void drawFrontArt(PoseStack poseStack, MultiBufferSource buffer, ResourceLocation textureLocation) {
|
||||
drawTexturedQuad(poseStack, buffer, textureLocation, FRONT_Z, false);
|
||||
drawTexturedQuad(poseStack, buffer, textureLocation, ART_Z, false);
|
||||
}
|
||||
|
||||
private static void drawTexturedQuad(PoseStack poseStack, MultiBufferSource buffer, ResourceLocation textureLocation, float z, boolean reverseWinding) {
|
||||
|
||||
@@ -36,6 +36,44 @@ public final class BoardLocalSpace {
|
||||
};
|
||||
}
|
||||
|
||||
public static SlotCenter rewardCenter(int index, Direction boardFacing, int count) {
|
||||
if (count <= 0) {
|
||||
return new SlotCenter(0.5F, 0.22F);
|
||||
}
|
||||
float columnProgress = count == 1 ? 0.5F : -0.04F + (1.08F * index / (count - 1.0F));
|
||||
float rowProgress = 0.15F;
|
||||
HorizontalVector bottomVector = vectorFor(boardFacing);
|
||||
HorizontalVector rightVector = vectorFor(rightDirection(boardFacing));
|
||||
float x = 0.5F + bottomVector.x() * (rowProgress - 0.5F) + rightVector.x() * (columnProgress - 0.5F);
|
||||
float z = 0.5F + bottomVector.z() * (rowProgress - 0.5F) + rightVector.z() * (columnProgress - 0.5F);
|
||||
return new SlotCenter(x, z);
|
||||
}
|
||||
|
||||
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);
|
||||
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 nearestDistance <= 0.05D * 0.05D ? nearestIndex : -1;
|
||||
}
|
||||
|
||||
private static double projectProgress(double localX, double localZ, Direction direction) {
|
||||
HorizontalVector vector = vectorFor(direction);
|
||||
double offsetX = localX - 0.5D;
|
||||
|
||||
@@ -18,6 +18,9 @@ import net.minecraft.world.phys.BlockHitResult;
|
||||
public final class DuelSession {
|
||||
private enum Phase {
|
||||
PLAYING,
|
||||
PLAYER_ANIMATION,
|
||||
OPPONENT_ANIMATION,
|
||||
CLOSING_ANIMATION,
|
||||
OVERVIEW
|
||||
}
|
||||
|
||||
@@ -25,7 +28,9 @@ public final class DuelSession {
|
||||
private final MatchParticipant opponentParticipant;
|
||||
private final TriadMatch match;
|
||||
private final List<ResourceLocation> refundablePlayerCards;
|
||||
private final List<ResourceLocation> opponentStartingCards;
|
||||
private final boolean refundCardsOnEnd;
|
||||
private final boolean aiVsAi;
|
||||
private final BlockPos tablePos;
|
||||
private Phase phase;
|
||||
|
||||
@@ -36,12 +41,17 @@ public final class DuelSession {
|
||||
List<GameCard> opponentHand,
|
||||
List<ResourceLocation> refundablePlayerCards,
|
||||
boolean refundCardsOnEnd,
|
||||
boolean aiVsAi,
|
||||
BlockPos tablePos) {
|
||||
this.playerParticipant = new MatchParticipant(playerId, playerName);
|
||||
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();
|
||||
this.phase = Phase.PLAYING;
|
||||
}
|
||||
@@ -62,6 +72,114 @@ public final class DuelSession {
|
||||
return phase == Phase.OVERVIEW;
|
||||
}
|
||||
|
||||
public boolean isAnimating() {
|
||||
return phase == Phase.PLAYER_ANIMATION || phase == Phase.OPPONENT_ANIMATION;
|
||||
}
|
||||
|
||||
public boolean isAwaitingPlayerAnimation() {
|
||||
return phase == Phase.PLAYER_ANIMATION;
|
||||
}
|
||||
|
||||
public boolean isAwaitingOpponentAnimation() {
|
||||
return phase == Phase.OPPONENT_ANIMATION;
|
||||
}
|
||||
|
||||
public void beginPlayerAnimation() {
|
||||
phase = Phase.PLAYER_ANIMATION;
|
||||
}
|
||||
|
||||
public void beginOpponentAnimation() {
|
||||
phase = Phase.OPPONENT_ANIMATION;
|
||||
}
|
||||
|
||||
public void returnToPlaying() {
|
||||
phase = Phase.PLAYING;
|
||||
}
|
||||
|
||||
public void beginClosingAnimation() {
|
||||
phase = Phase.CLOSING_ANIMATION;
|
||||
}
|
||||
|
||||
public boolean isClosingAnimation() {
|
||||
return phase == Phase.CLOSING_ANIMATION;
|
||||
}
|
||||
|
||||
public boolean isAiVsAi() {
|
||||
return aiVsAi;
|
||||
}
|
||||
|
||||
public int playerHandCount() {
|
||||
return match.handFor(playerParticipant).size();
|
||||
}
|
||||
|
||||
public int opponentHandCount() {
|
||||
return match.handFor(opponentParticipant).size();
|
||||
}
|
||||
|
||||
public int playerScore() {
|
||||
return match.scoreFor(playerParticipant);
|
||||
}
|
||||
|
||||
public int opponentScore() {
|
||||
return match.scoreFor(opponentParticipant);
|
||||
}
|
||||
|
||||
public String rulesSummary() {
|
||||
List<String> rules = new ArrayList<>();
|
||||
if (match.ruleSet().sameRule()) {
|
||||
rules.add("Same");
|
||||
}
|
||||
if (match.ruleSet().sameWallRule()) {
|
||||
rules.add("Same Wall");
|
||||
}
|
||||
if (match.ruleSet().plusRule()) {
|
||||
rules.add("Plus");
|
||||
}
|
||||
if (match.ruleSet().openHands()) {
|
||||
rules.add("Open");
|
||||
}
|
||||
if (rules.isEmpty()) {
|
||||
return "None";
|
||||
}
|
||||
return String.join(", ", rules);
|
||||
}
|
||||
|
||||
public String turnIndicator() {
|
||||
return isPlayerTurn() ? "P1" : "P2";
|
||||
}
|
||||
|
||||
public String gameStateLabel() {
|
||||
return switch (phase) {
|
||||
case PLAYING -> "Playing";
|
||||
case PLAYER_ANIMATION, OPPONENT_ANIMATION -> "Animating";
|
||||
case CLOSING_ANIMATION -> "Closing";
|
||||
case OVERVIEW -> "Overview";
|
||||
};
|
||||
}
|
||||
|
||||
public String nextStepInstruction() {
|
||||
if (isComplete()) {
|
||||
int playerScore = playerScore();
|
||||
int opponentScore = opponentScore();
|
||||
String winner = playerScore == opponentScore ? "Draw" : playerScore > opponentScore ? "P1" : "P2";
|
||||
if (isInOverview()) {
|
||||
return opponentRewardCards().isEmpty()
|
||||
? "Ready for next game"
|
||||
: winner + " choosing 1 cards to keep";
|
||||
}
|
||||
return "Game over - " + winner + (winner.equals("Draw") ? "" : " wins");
|
||||
}
|
||||
|
||||
return switch (phase) {
|
||||
case PLAYER_ANIMATION, OPPONENT_ANIMATION -> "Resolving animations";
|
||||
case CLOSING_ANIMATION -> "Clearing board";
|
||||
case OVERVIEW -> "Choose a reward card or finish";
|
||||
case PLAYING -> isPlayerTurn()
|
||||
? "Waiting for you to place a card"
|
||||
: "Waiting for P2 to place a card";
|
||||
};
|
||||
}
|
||||
|
||||
public void enterOverview() {
|
||||
phase = Phase.OVERVIEW;
|
||||
}
|
||||
@@ -96,15 +214,23 @@ public final class DuelSession {
|
||||
}
|
||||
|
||||
public MoveResult playOpponentTurn() {
|
||||
return playAiTurnFor(opponentParticipant);
|
||||
}
|
||||
|
||||
public MoveResult playAiTurn() {
|
||||
return playAiTurnFor(match.activeParticipant());
|
||||
}
|
||||
|
||||
private MoveResult playAiTurnFor(MatchParticipant participant) {
|
||||
int bestHandIndex = -1;
|
||||
BoardCell bestCell = null;
|
||||
int bestCaptures = -1;
|
||||
|
||||
List<GameCard> opponentHand = match.handFor(opponentParticipant);
|
||||
for (int handIndex = 0; handIndex < opponentHand.size(); handIndex++) {
|
||||
List<GameCard> hand = match.handFor(participant);
|
||||
for (int handIndex = 0; handIndex < hand.size(); handIndex++) {
|
||||
for (BoardCell cell : openCells()) {
|
||||
TriadMatch probe = duplicateMatch();
|
||||
MoveResult result = probe.play(new TriadMove(opponentParticipant, handIndex, cell));
|
||||
MoveResult result = probe.play(new TriadMove(participant, handIndex, cell));
|
||||
if (!result.valid()) {
|
||||
continue;
|
||||
}
|
||||
@@ -121,7 +247,7 @@ public final class DuelSession {
|
||||
return MoveResult.failure("Opponent could not find a legal move");
|
||||
}
|
||||
|
||||
return match.play(new TriadMove(opponentParticipant, bestHandIndex, bestCell));
|
||||
return match.play(new TriadMove(participant, bestHandIndex, bestCell));
|
||||
}
|
||||
|
||||
public Component boardSummary() {
|
||||
@@ -160,7 +286,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<ResourceLocation> refundablePlayerCards() {
|
||||
@@ -195,6 +321,10 @@ public final class DuelSession {
|
||||
return refundCardsOnEnd;
|
||||
}
|
||||
|
||||
public List<ResourceLocation> opponentRewardCards() {
|
||||
return List.copyOf(opponentStartingCards);
|
||||
}
|
||||
|
||||
public boolean isAtTable(BlockPos blockPos) {
|
||||
return tablePos.equals(blockPos);
|
||||
}
|
||||
|
||||
@@ -6,6 +6,7 @@ import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.UUID;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
import java.util.Collections;
|
||||
|
||||
import com.trunksbomb.minetriad.card.CardRegistry;
|
||||
import com.trunksbomb.minetriad.card.CardDefinition;
|
||||
@@ -29,12 +30,27 @@ public final class DuelSessionManager {
|
||||
return ACTIVE_DUELS.get(player.getUUID());
|
||||
}
|
||||
|
||||
public static DuelSession getAt(BlockPos tablePos) {
|
||||
return ACTIVE_DUELS.values().stream()
|
||||
.filter(session -> session.isAtTable(tablePos))
|
||||
.findFirst()
|
||||
.orElse(null);
|
||||
}
|
||||
|
||||
public static DuelSession start(Player player, BlockPos tablePos) {
|
||||
return start(player, tablePos, false);
|
||||
}
|
||||
|
||||
public static DuelSession start(Player player, BlockPos tablePos, boolean aiVsAi) {
|
||||
List<GameCard> playerHand = buildPlayerHand(player.getInventory());
|
||||
if (playerHand.size() < 5) {
|
||||
if (!aiVsAi && playerHand.size() < 5) {
|
||||
throw new IllegalStateException("A duel requires 5 Triad Cards in your inventory");
|
||||
}
|
||||
|
||||
if (aiVsAi) {
|
||||
playerHand = buildRandomHand();
|
||||
}
|
||||
|
||||
List<ResourceLocation> refundableCards = playerHand.stream()
|
||||
.map(card -> card.definition().id())
|
||||
.toList();
|
||||
@@ -42,9 +58,10 @@ public final class DuelSessionManager {
|
||||
player.getUUID(),
|
||||
player.getName().getString(),
|
||||
playerHand,
|
||||
buildOpponentHand(),
|
||||
aiVsAi ? buildRandomHand() : buildOpponentHand(),
|
||||
refundableCards,
|
||||
!player.getAbilities().instabuild,
|
||||
!aiVsAi && !player.getAbilities().instabuild,
|
||||
aiVsAi,
|
||||
tablePos);
|
||||
ACTIVE_DUELS.put(player.getUUID(), session);
|
||||
return session;
|
||||
@@ -109,4 +126,13 @@ public final class DuelSessionManager {
|
||||
.map(GameCard::new)
|
||||
.toList();
|
||||
}
|
||||
|
||||
private static List<GameCard> buildRandomHand() {
|
||||
List<CardDefinition> cards = new ArrayList<>(CardRegistry.all());
|
||||
Collections.shuffle(cards);
|
||||
return cards.stream()
|
||||
.limit(5)
|
||||
.map(GameCard::new)
|
||||
.toList();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,21 +7,29 @@ import net.minecraft.resources.ResourceLocation;
|
||||
public record MoveResult(
|
||||
boolean valid,
|
||||
String errorMessage,
|
||||
List<List<BoardCell>> captureWaves,
|
||||
List<BoardCell> capturedCells,
|
||||
List<String> battleLog,
|
||||
String playedCardName,
|
||||
ResourceLocation playedCardId,
|
||||
BoardCell playedCell) {
|
||||
public static MoveResult success(
|
||||
List<BoardCell> capturedCells,
|
||||
List<List<BoardCell>> captureWaves,
|
||||
List<String> battleLog,
|
||||
String playedCardName,
|
||||
ResourceLocation playedCardId,
|
||||
BoardCell playedCell) {
|
||||
return new MoveResult(true, "", List.copyOf(capturedCells), List.copyOf(battleLog), playedCardName, playedCardId, playedCell);
|
||||
List<List<BoardCell>> immutableWaves = captureWaves.stream()
|
||||
.map(List::copyOf)
|
||||
.toList();
|
||||
List<BoardCell> flattenedCaptures = immutableWaves.stream()
|
||||
.flatMap(List::stream)
|
||||
.distinct()
|
||||
.toList();
|
||||
return new MoveResult(true, "", immutableWaves, flattenedCaptures, List.copyOf(battleLog), playedCardName, playedCardId, playedCell);
|
||||
}
|
||||
|
||||
public static MoveResult failure(String errorMessage) {
|
||||
return new MoveResult(false, errorMessage, List.of(), List.of(), "", null, null);
|
||||
return new MoveResult(false, errorMessage, List.of(), List.of(), List.of(), "", null, null);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,7 +2,11 @@ package com.trunksbomb.minetriad.game;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.LinkedHashSet;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
|
||||
public final class TriadMatch {
|
||||
private final MatchParticipant firstParticipant;
|
||||
@@ -75,14 +79,111 @@ public final class TriadMatch {
|
||||
board[move.targetCell().index()] = new PlacedCard(move.participant(), playedCard);
|
||||
|
||||
List<String> battleLog = new ArrayList<>();
|
||||
List<BoardCell> capturedCells = resolveCaptures(move.targetCell(), move.participant(), playedCard, battleLog);
|
||||
CaptureResolution captureResolution = resolveCaptures(move.targetCell(), move.participant(), playedCard, battleLog);
|
||||
activeParticipant = otherParticipant(move.participant());
|
||||
return MoveResult.success(capturedCells, battleLog, playedCard.definition().name().getString(), playedCard.definition().id(), move.targetCell());
|
||||
return MoveResult.success(captureResolution.captureWaves(), battleLog, playedCard.definition().name().getString(), playedCard.definition().id(), move.targetCell());
|
||||
}
|
||||
|
||||
private List<BoardCell> resolveCaptures(BoardCell origin, MatchParticipant owner, GameCard playedCard, List<String> battleLog) {
|
||||
List<BoardCell> captured = new ArrayList<>();
|
||||
private CaptureResolution resolveCaptures(BoardCell origin, MatchParticipant owner, GameCard playedCard, List<String> battleLog) {
|
||||
List<SideComparison> comparisons = buildComparisons(origin, owner, playedCard, battleLog);
|
||||
LinkedHashSet<BoardCell> firstWave = new LinkedHashSet<>();
|
||||
LinkedHashSet<BoardCell> comboSeeds = new LinkedHashSet<>();
|
||||
|
||||
for (SideComparison comparison : comparisons) {
|
||||
if (!comparison.neighbor().owner().equals(owner) && comparison.attackValue() > comparison.defendValue()) {
|
||||
firstWave.add(comparison.cell());
|
||||
}
|
||||
}
|
||||
|
||||
if (ruleSet.sameRule()) {
|
||||
List<SameCandidate> sameCandidates = buildSameCandidates(origin, playedCard, battleLog);
|
||||
long sameMatches = sameCandidates.stream().filter(SameCandidate::matches).count();
|
||||
if (sameMatches >= 2) {
|
||||
List<BoardCell> sameCaptures = sameCandidates.stream()
|
||||
.filter(SameCandidate::matches)
|
||||
.map(SameCandidate::cell)
|
||||
.filter(cell -> cell != null)
|
||||
.distinct()
|
||||
.filter(cell -> {
|
||||
PlacedCard placedCard = cardAt(cell);
|
||||
return placedCard != null && !placedCard.owner().equals(owner);
|
||||
})
|
||||
.toList();
|
||||
if (!sameCaptures.isEmpty()) {
|
||||
battleLog.add("Same triggers.");
|
||||
firstWave.addAll(sameCaptures);
|
||||
comboSeeds.addAll(sameCaptures);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (ruleSet.plusRule()) {
|
||||
Map<Integer, List<SideComparison>> plusGroups = new LinkedHashMap<>();
|
||||
for (SideComparison comparison : comparisons) {
|
||||
int sum = comparison.attackValue() + comparison.defendValue();
|
||||
plusGroups.computeIfAbsent(sum, ignored -> new ArrayList<>()).add(comparison);
|
||||
}
|
||||
for (Map.Entry<Integer, List<SideComparison>> entry : plusGroups.entrySet()) {
|
||||
if (entry.getValue().size() < 2) {
|
||||
continue;
|
||||
}
|
||||
List<BoardCell> plusCaptures = entry.getValue().stream()
|
||||
.map(SideComparison::cell)
|
||||
.distinct()
|
||||
.filter(cell -> {
|
||||
PlacedCard placedCard = cardAt(cell);
|
||||
return placedCard != null && !placedCard.owner().equals(owner);
|
||||
})
|
||||
.toList();
|
||||
if (!plusCaptures.isEmpty()) {
|
||||
battleLog.add("Plus triggers on sum " + entry.getKey() + ".");
|
||||
firstWave.addAll(plusCaptures);
|
||||
comboSeeds.addAll(plusCaptures);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
List<List<BoardCell>> waves = new ArrayList<>();
|
||||
if (!firstWave.isEmpty()) {
|
||||
applyOwnership(owner, firstWave);
|
||||
waves.add(List.copyOf(firstWave));
|
||||
}
|
||||
|
||||
if (!comboSeeds.isEmpty()) {
|
||||
LinkedHashSet<BoardCell> comboWave = new LinkedHashSet<>();
|
||||
for (BoardCell comboCell : comboSeeds) {
|
||||
PlacedCard comboCard = cardAt(comboCell);
|
||||
if (comboCard == null || !comboCard.owner().equals(owner)) {
|
||||
continue;
|
||||
}
|
||||
for (CardSide side : CardSide.values()) {
|
||||
BoardCell neighborCell = neighbor(comboCell, side);
|
||||
if (neighborCell == null) {
|
||||
continue;
|
||||
}
|
||||
PlacedCard neighbor = cardAt(neighborCell);
|
||||
if (neighbor == null || neighbor.owner().equals(owner)) {
|
||||
continue;
|
||||
}
|
||||
int attackValue = comboCard.card().value(side);
|
||||
int defendValue = neighbor.card().value(side.opposite());
|
||||
if (attackValue > defendValue) {
|
||||
comboWave.add(neighborCell);
|
||||
}
|
||||
}
|
||||
}
|
||||
if (!comboWave.isEmpty()) {
|
||||
battleLog.add("Combo triggers.");
|
||||
applyOwnership(owner, comboWave);
|
||||
waves.add(List.copyOf(comboWave));
|
||||
}
|
||||
}
|
||||
|
||||
return new CaptureResolution(waves);
|
||||
}
|
||||
|
||||
private List<SideComparison> buildComparisons(BoardCell origin, MatchParticipant owner, GameCard playedCard, List<String> battleLog) {
|
||||
List<SideComparison> comparisons = new ArrayList<>();
|
||||
for (CardSide side : CardSide.values()) {
|
||||
BoardCell neighborCell = neighbor(origin, side);
|
||||
if (neighborCell == null) {
|
||||
@@ -90,13 +191,12 @@ public final class TriadMatch {
|
||||
}
|
||||
|
||||
PlacedCard neighbor = cardAt(neighborCell);
|
||||
if (neighbor == null || neighbor.owner().equals(owner)) {
|
||||
if (neighbor == null) {
|
||||
continue;
|
||||
}
|
||||
|
||||
int attackValue = playedCard.value(side);
|
||||
int defendValue = neighbor.card().value(side.opposite());
|
||||
boolean capturedNeighbor = attackValue > defendValue;
|
||||
battleLog.add(String.format(
|
||||
"%s %s=%d vs %s %s=%d at [%d,%d] -> %s",
|
||||
playedCard.definition().name().getString(),
|
||||
@@ -107,14 +207,47 @@ public final class TriadMatch {
|
||||
defendValue,
|
||||
neighborCell.row(),
|
||||
neighborCell.column(),
|
||||
capturedNeighbor ? "flip" : "hold"));
|
||||
if (capturedNeighbor) {
|
||||
board[neighborCell.index()] = new PlacedCard(owner, neighbor.card());
|
||||
captured.add(neighborCell);
|
||||
!neighbor.owner().equals(owner) && attackValue > defendValue ? "flip" : "hold"));
|
||||
comparisons.add(new SideComparison(side, neighborCell, neighbor, attackValue, defendValue));
|
||||
}
|
||||
return comparisons;
|
||||
}
|
||||
|
||||
return captured;
|
||||
private List<SameCandidate> buildSameCandidates(BoardCell origin, GameCard playedCard, List<String> battleLog) {
|
||||
List<SameCandidate> candidates = new ArrayList<>();
|
||||
for (CardSide side : CardSide.values()) {
|
||||
BoardCell neighborCell = neighbor(origin, side);
|
||||
if (neighborCell == null) {
|
||||
boolean wallMatch = ruleSet.sameWallRule() && playedCard.value(side) == 10;
|
||||
battleLog.add(String.format(
|
||||
"%s %s=%d vs WALL=%d -> %s",
|
||||
playedCard.definition().name().getString(),
|
||||
side.name(),
|
||||
playedCard.value(side),
|
||||
10,
|
||||
wallMatch ? "same" : "ignore"));
|
||||
candidates.add(new SameCandidate(side, null, wallMatch));
|
||||
continue;
|
||||
}
|
||||
|
||||
PlacedCard neighbor = cardAt(neighborCell);
|
||||
if (neighbor == null) {
|
||||
continue;
|
||||
}
|
||||
|
||||
boolean sameMatch = playedCard.value(side) == neighbor.card().value(side.opposite());
|
||||
candidates.add(new SameCandidate(side, neighborCell, sameMatch));
|
||||
}
|
||||
return candidates;
|
||||
}
|
||||
|
||||
private void applyOwnership(MatchParticipant owner, Set<BoardCell> capturedCells) {
|
||||
for (BoardCell cell : capturedCells) {
|
||||
PlacedCard placedCard = cardAt(cell);
|
||||
if (placedCard != null) {
|
||||
board[cell.index()] = new PlacedCard(owner, placedCard.card());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private BoardCell neighbor(BoardCell cell, CardSide side) {
|
||||
@@ -167,4 +300,13 @@ public final class TriadMatch {
|
||||
void forceActiveParticipant(MatchParticipant participant) {
|
||||
this.activeParticipant = participant;
|
||||
}
|
||||
|
||||
private record SideComparison(CardSide side, BoardCell cell, PlacedCard neighbor, int attackValue, int defendValue) {
|
||||
}
|
||||
|
||||
private record SameCandidate(CardSide side, BoardCell cell, boolean matches) {
|
||||
}
|
||||
|
||||
private record CaptureResolution(List<List<BoardCell>> captureWaves) {
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,7 +3,8 @@ package com.trunksbomb.minetriad.game;
|
||||
public record TriadRuleSet(
|
||||
boolean openHands,
|
||||
boolean sameRule,
|
||||
boolean sameWallRule,
|
||||
boolean plusRule,
|
||||
boolean elementalRule) {
|
||||
public static final TriadRuleSet CLASSIC_OPEN = new TriadRuleSet(true, false, false, false);
|
||||
public static final TriadRuleSet CLASSIC_OPEN = new TriadRuleSet(true, true, true, true, false);
|
||||
}
|
||||
|
||||
@@ -2,8 +2,10 @@ package com.trunksbomb.minetriad.world;
|
||||
|
||||
import com.mojang.serialization.MapCodec;
|
||||
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;
|
||||
@@ -19,18 +21,28 @@ 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;
|
||||
import net.minecraft.world.level.block.Block;
|
||||
import net.minecraft.world.level.block.entity.BlockEntity;
|
||||
import net.minecraft.world.level.block.entity.BlockEntityTicker;
|
||||
import net.minecraft.world.level.block.entity.BlockEntityType;
|
||||
import net.minecraft.world.level.block.state.StateDefinition;
|
||||
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);
|
||||
@@ -53,57 +65,60 @@ 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) {
|
||||
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;
|
||||
}
|
||||
|
||||
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 {
|
||||
session = DuelSessionManager.start(player, pos);
|
||||
session = DuelSessionManager.start(player, pos, aiVsAi);
|
||||
} catch (IllegalStateException exception) {
|
||||
player.displayClientMessage(Component.literal("You need 5 Triad Cards in your inventory to start a duel.").withStyle(ChatFormatting.YELLOW), false);
|
||||
return ItemInteractionResult.CONSUME;
|
||||
}
|
||||
table.clearBoard();
|
||||
table.setParticipants(session.playerParticipantId(), session.opponentParticipantId());
|
||||
table.updateHudState(session);
|
||||
if (aiVsAi) {
|
||||
player.displayClientMessage(Component.literal("AI vs AI duel started. Empty-hand click on a Duel Table launches an autoplay match.").withStyle(ChatFormatting.GOLD), false);
|
||||
if (player instanceof net.minecraft.server.level.ServerPlayer serverPlayer) {
|
||||
DuelTableAnimationResolver.startAiTurn(table, session, serverPlayer);
|
||||
}
|
||||
} 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 (!session.isPlayerTurn()) {
|
||||
MoveResult opponentMove = session.playOpponentTurn();
|
||||
if (!opponentMove.valid()) {
|
||||
player.displayClientMessage(Component.literal(opponentMove.errorMessage()).withStyle(ChatFormatting.RED), false);
|
||||
clearAndRefund(level, pos, player, true);
|
||||
return ItemInteractionResult.CONSUME;
|
||||
}
|
||||
placeBoardCard(table, opponentMove, DuelTableBlockEntity.OWNER_SECOND);
|
||||
applyCapturedOwnership(table, opponentMove, DuelTableBlockEntity.OWNER_SECOND);
|
||||
player.displayClientMessage(Component.literal("Training Duelist takes a turn."), false);
|
||||
sendBattleLog(player, opponentMove, ChatFormatting.GRAY);
|
||||
player.displayClientMessage(session.boardSummary(), false);
|
||||
finishIfComplete(level, pos, player, session);
|
||||
if (table.hasActiveAnimation() || session.isAnimating()) {
|
||||
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 +131,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;
|
||||
}
|
||||
|
||||
@@ -126,36 +139,14 @@ public class DuelTableBlock extends BaseEntityBlock {
|
||||
clearAndRefund(level, pos, player, true);
|
||||
return ItemInteractionResult.CONSUME;
|
||||
}
|
||||
applyCapturedOwnership(table, playerMove, DuelTableBlockEntity.OWNER_FIRST);
|
||||
|
||||
if (!player.getAbilities().instabuild) {
|
||||
stack.consume(1, player);
|
||||
}
|
||||
|
||||
player.displayClientMessage(Component.literal("You play " + playerMove.playedCardName() + "."), false);
|
||||
sendBattleLog(player, playerMove, ChatFormatting.DARK_AQUA);
|
||||
player.displayClientMessage(session.boardSummary(), false);
|
||||
finishIfComplete(level, pos, player, session);
|
||||
if (session.isComplete()) {
|
||||
return ItemInteractionResult.CONSUME;
|
||||
}
|
||||
|
||||
MoveResult opponentMove = session.playOpponentTurn();
|
||||
if (!opponentMove.valid()) {
|
||||
player.displayClientMessage(Component.literal(opponentMove.errorMessage()).withStyle(ChatFormatting.RED), false);
|
||||
clearAndRefund(level, pos, player, true);
|
||||
return ItemInteractionResult.CONSUME;
|
||||
}
|
||||
placeBoardCard(table, opponentMove, DuelTableBlockEntity.OWNER_SECOND);
|
||||
applyCapturedOwnership(table, opponentMove, DuelTableBlockEntity.OWNER_SECOND);
|
||||
player.displayClientMessage(Component.literal("Training Duelist answers with " + opponentMove.playedCardName() + "."), false);
|
||||
sendBattleLog(player, opponentMove, ChatFormatting.GRAY);
|
||||
player.displayClientMessage(session.boardSummary(), false);
|
||||
finishIfComplete(level, pos, player, session);
|
||||
if (!session.isComplete()) {
|
||||
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);
|
||||
}
|
||||
session.beginPlayerAnimation();
|
||||
table.updateHudState(session);
|
||||
table.startMoveAnimation(playerMove, DuelTableBlockEntity.OWNER_FIRST);
|
||||
return ItemInteractionResult.CONSUME;
|
||||
} catch (Exception exception) {
|
||||
MineTriad.LOGGER.error("Duel table interaction failed", exception);
|
||||
@@ -203,11 +194,64 @@ public class DuelTableBlock extends BaseEntityBlock {
|
||||
return new DuelTableBlockEntity(pos, state);
|
||||
}
|
||||
|
||||
@Override
|
||||
public <T extends BlockEntity> BlockEntityTicker<T> getTicker(Level level, BlockState state, BlockEntityType<T> blockEntityType) {
|
||||
return createTickerHelper(blockEntityType, com.trunksbomb.minetriad.registry.TriadBlockEntities.DUEL_TABLE.get(), DuelTableBlockEntity::tick);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected RenderShape getRenderShape(BlockState state) {
|
||||
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);
|
||||
@@ -225,33 +269,11 @@ public class DuelTableBlock extends BaseEntityBlock {
|
||||
return table.setCard(moveResult.playedCell().index(), CardItem.createCardStack(moveResult.playedCardId(), TriadItems.TRIAD_CARD.get()), owner);
|
||||
}
|
||||
|
||||
private static void applyCapturedOwnership(DuelTableBlockEntity table, MoveResult moveResult, int owner) {
|
||||
for (var cell : moveResult.capturedCells()) {
|
||||
table.setOwner(cell.index(), 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;
|
||||
}
|
||||
|
||||
private static void finishIfComplete(Level level, BlockPos pos, Player player, DuelSession session) {
|
||||
if (!session.isComplete()) {
|
||||
return;
|
||||
}
|
||||
|
||||
session.enterOverview();
|
||||
player.displayClientMessage(session.resultSummary().copy().withStyle(ChatFormatting.AQUA), false);
|
||||
player.displayClientMessage(session.overviewMessage().copy().withStyle(ChatFormatting.YELLOW), false);
|
||||
}
|
||||
|
||||
private static void clearAndRefund(Level level, BlockPos pos, Player player, boolean refundFullHand) {
|
||||
DuelTableBlockEntity table = getTableEntity(level, pos);
|
||||
if (table != null) {
|
||||
@@ -263,9 +285,98 @@ public class DuelTableBlock extends BaseEntityBlock {
|
||||
private static void completeOverview(Level level, BlockPos pos, Player player, DuelSession session) {
|
||||
DuelTableBlockEntity table = getTableEntity(level, pos);
|
||||
if (table != null) {
|
||||
table.clearBoard();
|
||||
}
|
||||
DuelSessionManager.end(player, true);
|
||||
player.displayClientMessage(Component.literal("Duel finished. All played cards have been returned.").withStyle(ChatFormatting.GREEN), false);
|
||||
session.beginClosingAnimation();
|
||||
table.updateHudState(session);
|
||||
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) {
|
||||
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;
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
@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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user