cards now have some simple animations: idle bob on the board, placement, card capture flip, game over wave, and end duel pop up
This commit is contained in:
@@ -0,0 +1,122 @@
|
||||
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.startMoveAnimation(opponentMove, DuelTableBlockEntity.OWNER_SECOND);
|
||||
player.displayClientMessage(Component.literal("Training Duelist answers with " + opponentMove.playedCardName() + ".").withStyle(ChatFormatting.GRAY), false);
|
||||
sendBattleLog(player, opponentMove, ChatFormatting.GRAY);
|
||||
}
|
||||
|
||||
static void handleAfterOpponentAnimation(Level level, BlockPos pos, DuelTableBlockEntity table, DuelSession session) {
|
||||
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();
|
||||
player.displayClientMessage(session.boardSummary(), false);
|
||||
player.displayClientMessage(Component.literal("Your turn. Hold one of your remaining duel cards and click an open space.").withStyle(ChatFormatting.YELLOW), false);
|
||||
player.displayClientMessage(session.handSummary(), false);
|
||||
}
|
||||
|
||||
public static void startAiTurn(DuelTableBlockEntity table, DuelSession session, ServerPlayer player) {
|
||||
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.startMoveAnimation(aiMove, owner);
|
||||
player.displayClientMessage(Component.literal((owner == DuelTableBlockEntity.OWNER_FIRST ? "Blue AI" : "Red AI") + " plays " + aiMove.playedCardName() + ".")
|
||||
.withStyle(ChatFormatting.GRAY), false);
|
||||
sendBattleLog(player, aiMove, ChatFormatting.GRAY);
|
||||
}
|
||||
|
||||
private static void enterOverview(DuelTableBlockEntity table, DuelSession session, ServerPlayer player) {
|
||||
session.enterOverview();
|
||||
table.startOverviewSweepAnimation();
|
||||
player.displayClientMessage(session.resultSummary().copy().withStyle(ChatFormatting.AQUA), false);
|
||||
player.displayClientMessage(session.overviewMessage().copy().withStyle(ChatFormatting.YELLOW), false);
|
||||
}
|
||||
|
||||
private static void sendBattleLog(ServerPlayer player, MoveResult moveResult, ChatFormatting color) {
|
||||
for (String line : moveResult.battleLog()) {
|
||||
player.displayClientMessage(Component.literal(line).withStyle(color), false);
|
||||
}
|
||||
}
|
||||
|
||||
private static ServerPlayer getPlayer(Level level, DuelSession session) {
|
||||
return level.getServer() == null ? null : level.getServer().getPlayerList().getPlayer(session.playerParticipantId());
|
||||
}
|
||||
}
|
||||
@@ -1,17 +1,26 @@
|
||||
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.network.Connection;
|
||||
import net.minecraft.network.protocol.game.ClientboundBlockEntityDataPacket;
|
||||
import net.minecraft.network.chat.Component;
|
||||
import net.minecraft.world.ContainerHelper;
|
||||
import net.minecraft.world.item.ItemStack;
|
||||
import net.minecraft.world.level.Level;
|
||||
@@ -24,11 +33,26 @@ public class DuelTableBlockEntity extends BlockEntity {
|
||||
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 final NonNullList<ItemStack> boardCards = NonNullList.withSize(SLOT_COUNT, ItemStack.EMPTY);
|
||||
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 animationSlot = -1;
|
||||
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;
|
||||
|
||||
public DuelTableBlockEntity(BlockPos pos, BlockState blockState) {
|
||||
super(TriadBlockEntities.DUEL_TABLE.get(), pos, blockState);
|
||||
@@ -42,6 +66,10 @@ public class DuelTableBlockEntity extends BlockEntity {
|
||||
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,6 +78,52 @@ public class DuelTableBlockEntity extends BlockEntity {
|
||||
return Optional.ofNullable(secondParticipantId);
|
||||
}
|
||||
|
||||
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 animationSlot;
|
||||
}
|
||||
|
||||
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;
|
||||
@@ -63,6 +137,7 @@ public class DuelTableBlockEntity extends BlockEntity {
|
||||
|
||||
boardCards.set(slot, stack.copyWithCount(1));
|
||||
ownerSlots[slot] = owner;
|
||||
slotAges[slot] = 0;
|
||||
sync();
|
||||
return true;
|
||||
}
|
||||
@@ -79,7 +154,10 @@ 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();
|
||||
firstParticipantId = null;
|
||||
secondParticipantId = null;
|
||||
sync();
|
||||
@@ -92,18 +170,106 @@ 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();
|
||||
firstParticipantId = null;
|
||||
secondParticipantId = null;
|
||||
sync();
|
||||
}
|
||||
|
||||
public void startMoveAnimation(MoveResult moveResult, int owner) {
|
||||
pendingAnimations.clear();
|
||||
clearAnimationState();
|
||||
clearTableAnimationState();
|
||||
if (moveResult.playedCell() == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
pendingAnimations.addLast(new AnimationStep(AnimationType.PLACEMENT, moveResult.playedCell().index(), owner));
|
||||
for (BoardCell capturedCell : moveResult.capturedCells()) {
|
||||
pendingAnimations.addLast(new AnimationStep(AnimationType.CAPTURE, capturedCell.index(), 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)) {
|
||||
blockEntity.setOwner(blockEntity.animationSlot, 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void saveAdditional(CompoundTag tag, HolderLookup.Provider registries) {
|
||||
super.saveAdditional(tag, registries);
|
||||
ContainerHelper.saveAllItems(tag, boardCards, registries);
|
||||
tag.putIntArray("OwnerSlots", ownerSlots);
|
||||
tag.putIntArray("SlotAges", slotAges);
|
||||
saveAnimation(tag);
|
||||
saveTableAnimation(tag);
|
||||
if (firstParticipantId != null) {
|
||||
tag.putUUID("FirstParticipantId", firstParticipantId);
|
||||
}
|
||||
@@ -118,6 +284,8 @@ public class DuelTableBlockEntity extends BlockEntity {
|
||||
clearBoardContents();
|
||||
ContainerHelper.loadAllItems(tag, boardCards, registries);
|
||||
loadOwnerData(tag);
|
||||
loadAnimation(tag);
|
||||
loadTableAnimation(tag);
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -125,6 +293,9 @@ public class DuelTableBlockEntity extends BlockEntity {
|
||||
CompoundTag tag = super.getUpdateTag(registries);
|
||||
ContainerHelper.saveAllItems(tag, boardCards, registries);
|
||||
tag.putIntArray("OwnerSlots", ownerSlots);
|
||||
tag.putIntArray("SlotAges", slotAges);
|
||||
saveAnimation(tag);
|
||||
saveTableAnimation(tag);
|
||||
if (firstParticipantId != null) {
|
||||
tag.putUUID("FirstParticipantId", firstParticipantId);
|
||||
}
|
||||
@@ -146,6 +317,8 @@ public class DuelTableBlockEntity extends BlockEntity {
|
||||
clearBoardContents();
|
||||
ContainerHelper.loadAllItems(tag, boardCards, registries);
|
||||
loadOwnerData(tag);
|
||||
loadAnimation(tag);
|
||||
loadTableAnimation(tag);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -153,7 +326,10 @@ 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();
|
||||
firstParticipantId = null;
|
||||
secondParticipantId = null;
|
||||
}
|
||||
@@ -163,14 +339,153 @@ 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 saveAnimation(CompoundTag tag) {
|
||||
tag.putString("AnimationType", animationType.name());
|
||||
tag.putInt("AnimationSlot", animationSlot);
|
||||
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"));
|
||||
animationSlot = tag.getInt("AnimationSlot");
|
||||
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();
|
||||
animationSlot = nextStep.slot();
|
||||
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;
|
||||
animationSlot = -1;
|
||||
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 slot, int targetOwner) {
|
||||
}
|
||||
}
|
||||
|
||||
@@ -14,6 +14,10 @@ import net.minecraft.client.renderer.blockentity.BlockEntityRendererProvider;
|
||||
import net.minecraft.world.item.ItemStack;
|
||||
|
||||
public class DuelTableBlockEntityRenderer implements BlockEntityRenderer<DuelTableBlockEntity> {
|
||||
private static final float TABLE_CARD_Y = 1.066F;
|
||||
private static final float IDLE_BOB_HEIGHT = 0.012F;
|
||||
private static final float IDLE_BOB_SPEED = 0.08F;
|
||||
|
||||
public DuelTableBlockEntityRenderer(BlockEntityRendererProvider.Context context) {
|
||||
}
|
||||
|
||||
@@ -27,10 +31,41 @@ 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.animationSlot() == 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 if (!blockEntity.hasActiveAnimation()) {
|
||||
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)));
|
||||
@@ -38,6 +73,21 @@ public class DuelTableBlockEntityRenderer implements BlockEntityRenderer<DuelTab
|
||||
}
|
||||
}
|
||||
|
||||
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 TriadCardItemRenderer.CardPalette perspectivePalette(DuelTableBlockEntity blockEntity, int owner) {
|
||||
if (owner == DuelTableBlockEntity.OWNER_NONE || Minecraft.getInstance().player == null) {
|
||||
return new TriadCardItemRenderer.CardPalette(
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -26,6 +29,7 @@ public final class DuelSession {
|
||||
private final TriadMatch match;
|
||||
private final List<ResourceLocation> refundablePlayerCards;
|
||||
private final boolean refundCardsOnEnd;
|
||||
private final boolean aiVsAi;
|
||||
private final BlockPos tablePos;
|
||||
private Phase phase;
|
||||
|
||||
@@ -36,12 +40,14 @@ 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.refundCardsOnEnd = refundCardsOnEnd;
|
||||
this.aiVsAi = aiVsAi;
|
||||
this.tablePos = tablePos.immutable();
|
||||
this.phase = Phase.PLAYING;
|
||||
}
|
||||
@@ -62,6 +68,42 @@ 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 void enterOverview() {
|
||||
phase = Phase.OVERVIEW;
|
||||
}
|
||||
@@ -96,15 +138,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 +171,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() {
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,6 +2,7 @@ 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.DuelSession;
|
||||
@@ -26,6 +27,8 @@ 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;
|
||||
@@ -67,16 +70,24 @@ public class DuelTableBlock extends BaseEntityBlock {
|
||||
|
||||
DuelSession session = DuelSessionManager.get(player);
|
||||
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());
|
||||
player.displayClientMessage(session.startMessage().copy().withStyle(ChatFormatting.GOLD), false);
|
||||
player.displayClientMessage(session.handSummary(), false);
|
||||
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;
|
||||
}
|
||||
|
||||
@@ -85,19 +96,8 @@ public class DuelTableBlock extends BaseEntityBlock {
|
||||
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()) {
|
||||
player.displayClientMessage(Component.literal("Wait for the current card animation to finish.").withStyle(ChatFormatting.YELLOW), false);
|
||||
return ItemInteractionResult.CONSUME;
|
||||
}
|
||||
|
||||
@@ -126,36 +126,15 @@ 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);
|
||||
}
|
||||
|
||||
session.beginPlayerAnimation();
|
||||
table.startMoveAnimation(playerMove, DuelTableBlockEntity.OWNER_FIRST);
|
||||
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);
|
||||
}
|
||||
return ItemInteractionResult.CONSUME;
|
||||
} catch (Exception exception) {
|
||||
MineTriad.LOGGER.error("Duel table interaction failed", exception);
|
||||
@@ -203,6 +182,11 @@ 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;
|
||||
@@ -225,12 +209,6 @@ 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);
|
||||
@@ -242,16 +220,6 @@ public class DuelTableBlock extends BaseEntityBlock {
|
||||
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 +231,8 @@ 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();
|
||||
session.beginClosingAnimation();
|
||||
table.startClearWaveAnimation();
|
||||
}
|
||||
DuelSessionManager.end(player, true);
|
||||
player.displayClientMessage(Component.literal("Duel finished. All played cards have been returned.").withStyle(ChatFormatting.GREEN), false);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user