diff --git a/src/main/java/xyz/nucleoid/plasmid/api/game/common/GameWaitingLobby.java b/src/main/java/xyz/nucleoid/plasmid/api/game/common/GameWaitingLobby.java index f4d9d643..a6a44470 100644 --- a/src/main/java/xyz/nucleoid/plasmid/api/game/common/GameWaitingLobby.java +++ b/src/main/java/xyz/nucleoid/plasmid/api/game/common/GameWaitingLobby.java @@ -1,5 +1,6 @@ package xyz.nucleoid.plasmid.api.game.common; +import eu.pb4.polymer.core.api.utils.PolymerUtils; import net.minecraft.entity.boss.BossBar; import net.minecraft.screen.ScreenTexts; import net.minecraft.server.network.ServerPlayerEntity; @@ -13,14 +14,18 @@ import xyz.nucleoid.plasmid.api.game.*; import xyz.nucleoid.plasmid.api.game.common.config.WaitingLobbyConfig; import xyz.nucleoid.plasmid.api.game.common.team.TeamSelectionLobby; +import xyz.nucleoid.plasmid.api.game.common.ui.WaitingLobbyUiLayout; import xyz.nucleoid.plasmid.api.game.common.widget.BossBarWidget; import xyz.nucleoid.plasmid.api.game.config.GameConfig; import xyz.nucleoid.plasmid.api.game.event.GameActivityEvents; import xyz.nucleoid.plasmid.api.game.event.GamePlayerEvents; +import xyz.nucleoid.plasmid.api.game.event.GameWaitingLobbyEvents; import xyz.nucleoid.plasmid.api.game.player.JoinOffer; import xyz.nucleoid.plasmid.api.game.player.JoinOfferResult; import xyz.nucleoid.plasmid.api.game.rule.GameRuleType; import xyz.nucleoid.plasmid.api.game.common.widget.SidebarWidget; +import xyz.nucleoid.plasmid.impl.game.common.ui.WaitingLobbyUi; +import xyz.nucleoid.plasmid.impl.game.common.ui.element.LeaveGameWaitingLobbyUiElement; import xyz.nucleoid.plasmid.impl.game.manager.GameSpaceManagerImpl; import xyz.nucleoid.plasmid.impl.compatibility.AfkDisplayCompatibility; @@ -89,7 +94,9 @@ public static GameWaitingLobby addTo(GameActivity activity, WaitingLobbyConfig p activity.listen(GameActivityEvents.TICK, lobby::onTick); activity.listen(GameActivityEvents.REQUEST_START, lobby::requestStart); activity.listen(GamePlayerEvents.OFFER, lobby::offerPlayer); + activity.listen(GamePlayerEvents.ADD, lobby::onAddPlayer); activity.listen(GamePlayerEvents.REMOVE, lobby::onRemovePlayer); + activity.listen(GameWaitingLobbyEvents.BUILD_UI_LAYOUT, lobby::onBuildUiLayout); activity.listen(GameActivityEvents.STATE_UPDATE, lobby::updateState); @@ -155,6 +162,11 @@ private void onTick() { this.started = false; this.startRequested = false; this.countdownStart = -1; + } else { + for (var player : this.gameSpace.getPlayers()) { + player.closeHandledScreen(); + PolymerUtils.reloadInventory(player); + } } } } @@ -180,10 +192,19 @@ private JoinOfferResult offerPlayer(JoinOffer offer) { return offer.pass(); } + private void onAddPlayer(ServerPlayerEntity player) { + var ui = new WaitingLobbyUi(player, this.gameSpace); + ui.open(); + } + private void onRemovePlayer(ServerPlayerEntity player) { this.updateCountdown(); } + private void onBuildUiLayout(WaitingLobbyUiLayout layout, ServerPlayerEntity player) { + layout.addTrailing(new LeaveGameWaitingLobbyUiElement(this.gameSpace, player)); + } + private void updateCountdown() { long targetDuration = this.getTargetCountdownDuration(); if (targetDuration != this.countdownDuration) { diff --git a/src/main/java/xyz/nucleoid/plasmid/api/game/common/team/TeamSelectionLobby.java b/src/main/java/xyz/nucleoid/plasmid/api/game/common/team/TeamSelectionLobby.java index 4ea63565..da770310 100644 --- a/src/main/java/xyz/nucleoid/plasmid/api/game/common/team/TeamSelectionLobby.java +++ b/src/main/java/xyz/nucleoid/plasmid/api/game/common/team/TeamSelectionLobby.java @@ -1,27 +1,18 @@ package xyz.nucleoid.plasmid.api.game.common.team; -import com.mojang.serialization.Codec; import it.unimi.dsi.fastutil.objects.Object2ObjectOpenHashMap; import it.unimi.dsi.fastutil.objects.Reference2IntMap; import it.unimi.dsi.fastutil.objects.Reference2IntMaps; import it.unimi.dsi.fastutil.objects.Reference2IntOpenHashMap; -import net.minecraft.component.DataComponentTypes; -import net.minecraft.component.type.NbtComponent; -import net.minecraft.item.ItemStack; -import net.minecraft.nbt.NbtOps; -import net.minecraft.registry.tag.ItemTags; import net.minecraft.server.network.ServerPlayerEntity; import net.minecraft.text.Text; -import net.minecraft.util.ActionResult; -import net.minecraft.util.Formatting; -import net.minecraft.util.Hand; -import xyz.nucleoid.plasmid.api.game.common.GameWaitingLobby; -import xyz.nucleoid.plasmid.impl.Plasmid; import xyz.nucleoid.plasmid.api.game.GameActivity; -import xyz.nucleoid.plasmid.api.game.event.GamePlayerEvents; +import xyz.nucleoid.plasmid.api.game.GameSpace; +import xyz.nucleoid.plasmid.api.game.common.GameWaitingLobby; +import xyz.nucleoid.plasmid.api.game.common.ui.WaitingLobbyUiLayout; +import xyz.nucleoid.plasmid.api.game.event.GameWaitingLobbyEvents; import xyz.nucleoid.plasmid.api.game.player.PlayerIterable; -import xyz.nucleoid.plasmid.api.util.ColoredBlocks; -import xyz.nucleoid.stimuli.event.item.ItemUseEvent; +import xyz.nucleoid.plasmid.impl.game.common.ui.element.TeamSelectionWaitingLobbyUiElement; import java.util.Map; import java.util.UUID; @@ -40,14 +31,14 @@ * @see GameWaitingLobby */ public final class TeamSelectionLobby { - private static final String TEAM_KEY = Plasmid.ID + ":team"; - + private final GameSpace gameSpace; private final GameTeamList teams; private final Reference2IntMap maxTeamSize = new Reference2IntOpenHashMap<>(); private final Map teamPreference = new Object2ObjectOpenHashMap<>(); - private TeamSelectionLobby(GameTeamList teams) { + private TeamSelectionLobby(GameSpace gameSpace, GameTeamList teams) { + this.gameSpace = gameSpace; this.teams = teams; } @@ -60,9 +51,8 @@ private TeamSelectionLobby(GameTeamList teams) { * @see TeamSelectionLobby#allocate(PlayerIterable, BiConsumer) */ public static TeamSelectionLobby addTo(GameActivity activity, GameTeamList teams) { - var lobby = new TeamSelectionLobby(teams); - activity.listen(GamePlayerEvents.ADD, lobby::onAddPlayer); - activity.listen(ItemUseEvent.EVENT, lobby::onUseItem); + var lobby = new TeamSelectionLobby(activity.getGameSpace(), teams); + activity.listen(GameWaitingLobbyEvents.BUILD_UI_LAYOUT, lobby::onBuildUiLayout); return lobby; } @@ -77,46 +67,28 @@ public void setSizeForTeam(GameTeamKey team, int size) { this.maxTeamSize.put(team, size); } - private void onAddPlayer(ServerPlayerEntity player) { - int index = 0; - - for (var team : this.teams) { - var config = team.config(); - var name = Text.translatable("text.plasmid.team_selection.request_team", config.name()) - .formatted(Formatting.BOLD, config.chatFormatting()); - - var stack = new ItemStack(ColoredBlocks.wool(config.blockDyeColor())); - stack.set(DataComponentTypes.ITEM_NAME, name); - - stack.set(DataComponentTypes.CUSTOM_DATA, NbtComponent.DEFAULT.with(NbtOps.INSTANCE, Codec.STRING.fieldOf(TEAM_KEY), team.key().id()).getOrThrow()); - - player.getInventory().setStack(index++, stack); + private void onBuildUiLayout(WaitingLobbyUiLayout layout, ServerPlayerEntity player) { + // Spectators cannot choose a team + if (!this.gameSpace.getPlayers().participants().contains(player)) { + return; } - } - - private ActionResult onUseItem(ServerPlayerEntity player, Hand hand) { - var stack = player.getStackInHand(hand); - - if (stack.isIn(ItemTags.WOOL)) { - var key = new GameTeamKey(stack.getOrDefault(DataComponentTypes.CUSTOM_DATA, NbtComponent.DEFAULT) - .get(Codec.STRING.fieldOf(TEAM_KEY)).getOrThrow()); + layout.addLeading(new TeamSelectionWaitingLobbyUiElement(teams, key -> { + return key == this.teamPreference.get(player.getUuid()); + }, key -> { var team = this.teams.byKey(key); if (team != null) { var config = team.config(); this.teamPreference.put(player.getUuid(), key); + layout.refresh(); var message = Text.translatable("text.plasmid.team_selection.requested_team", Text.translatable("text.plasmid.team_selection.suffixed_team", config.name()).formatted(config.chatFormatting())); player.sendMessage(message, false); - - return ActionResult.SUCCESS_SERVER; } - } - - return ActionResult.PASS; + })); } /** diff --git a/src/main/java/xyz/nucleoid/plasmid/api/game/common/ui/WaitingLobbyUiElement.java b/src/main/java/xyz/nucleoid/plasmid/api/game/common/ui/WaitingLobbyUiElement.java new file mode 100644 index 00000000..3a83bd3d --- /dev/null +++ b/src/main/java/xyz/nucleoid/plasmid/api/game/common/ui/WaitingLobbyUiElement.java @@ -0,0 +1,21 @@ +package xyz.nucleoid.plasmid.api.game.common.ui; + +import java.util.List; +import java.util.SequencedCollection; + +import eu.pb4.sgui.api.ClickType; +import eu.pb4.sgui.api.elements.GuiElementInterface; +import eu.pb4.sgui.api.gui.HotbarGui; +import eu.pb4.sgui.api.gui.SlotGuiInterface; + +public interface WaitingLobbyUiElement { + GuiElementInterface createMainElement(); + + default SequencedCollection createExtendedElements() { + return List.of(this.createMainElement()); + } + + static boolean isClick(ClickType type, SlotGuiInterface gui) { + return type.isRight || !(gui instanceof HotbarGui); + } +} diff --git a/src/main/java/xyz/nucleoid/plasmid/api/game/common/ui/WaitingLobbyUiLayout.java b/src/main/java/xyz/nucleoid/plasmid/api/game/common/ui/WaitingLobbyUiLayout.java new file mode 100644 index 00000000..f7629aaf --- /dev/null +++ b/src/main/java/xyz/nucleoid/plasmid/api/game/common/ui/WaitingLobbyUiLayout.java @@ -0,0 +1,19 @@ +package xyz.nucleoid.plasmid.api.game.common.ui; + +import eu.pb4.sgui.api.elements.GuiElementInterface; +import xyz.nucleoid.plasmid.impl.game.common.ui.WaitingLobbyUiLayoutImpl; + +import java.util.Objects; +import java.util.function.Consumer; + +public interface WaitingLobbyUiLayout { + void addLeading(WaitingLobbyUiElement element); + + void addTrailing(WaitingLobbyUiElement element); + + void refresh(); + + static WaitingLobbyUiLayout of(Consumer callback) { + return new WaitingLobbyUiLayoutImpl(Objects.requireNonNull(callback)); + } +} diff --git a/src/main/java/xyz/nucleoid/plasmid/api/game/event/GameWaitingLobbyEvents.java b/src/main/java/xyz/nucleoid/plasmid/api/game/event/GameWaitingLobbyEvents.java new file mode 100644 index 00000000..b73f5d5a --- /dev/null +++ b/src/main/java/xyz/nucleoid/plasmid/api/game/event/GameWaitingLobbyEvents.java @@ -0,0 +1,33 @@ +package xyz.nucleoid.plasmid.api.game.event; + +import net.minecraft.server.network.ServerPlayerEntity; +import xyz.nucleoid.plasmid.api.game.GameActivity; +import xyz.nucleoid.plasmid.api.game.GameSpace; +import xyz.nucleoid.plasmid.api.game.common.GameWaitingLobby; +import xyz.nucleoid.plasmid.api.game.common.ui.WaitingLobbyUiLayout; +import xyz.nucleoid.stimuli.event.StimulusEvent; + +/** + * Events relating to a {@link GameWaitingLobby} applied to a {@link GameActivity} within a {@link GameSpace}. + */ +public final class GameWaitingLobbyEvents { + /** + * Called when a {@link WaitingLobbyUiLayout} is created for a specific player's waiting lobby UI. + *

+ * This event should be used to add custom UI elements to the hotbar UI + * used by players before the game begins. + */ + public static final StimulusEvent BUILD_UI_LAYOUT = StimulusEvent.create(BuildUiLayout.class, ctx -> (layout, player) -> { + try { + for (var listener : ctx.getListeners()) { + listener.onBuildUiLayout(layout, player); + } + } catch (Throwable throwable) { + ctx.handleException(throwable); + } + }); + + public interface BuildUiLayout { + void onBuildUiLayout(WaitingLobbyUiLayout layout, ServerPlayerEntity player); + } +} diff --git a/src/main/java/xyz/nucleoid/plasmid/api/util/Guis.java b/src/main/java/xyz/nucleoid/plasmid/api/util/Guis.java index 0848e947..28acdf3d 100644 --- a/src/main/java/xyz/nucleoid/plasmid/api/util/Guis.java +++ b/src/main/java/xyz/nucleoid/plasmid/api/util/Guis.java @@ -1,5 +1,6 @@ package xyz.nucleoid.plasmid.api.util; +import eu.pb4.sgui.api.ClickType; import eu.pb4.sgui.api.SlotHolder; import eu.pb4.sgui.api.elements.GuiElementInterface; import eu.pb4.sgui.api.gui.SimpleGui; @@ -13,6 +14,7 @@ import net.minecraft.registry.*; import net.minecraft.screen.ScreenHandlerType; import net.minecraft.screen.ScreenTexts; +import net.minecraft.screen.slot.SlotActionType; import net.minecraft.server.network.ServerPlayerEntity; import net.minecraft.text.MutableText; import net.minecraft.util.DyeColor; @@ -21,29 +23,46 @@ import org.jetbrains.annotations.Range; import java.util.Collection; +import java.util.function.Consumer; public final class Guis { private Guis() { } - public static SimpleGui createSelectorGui(ServerPlayerEntity player, MutableText text, boolean includePlayerSlots, GuiElementInterface... elements) { - var gui = new SimpleGui(selectScreenType(elements.length), player, includePlayerSlots); + public static SimpleGui createSelectorGui(ServerPlayerEntity player, MutableText text, boolean includePlayerSlots, Consumer onClick, Consumer onClose, GuiElementInterface... elements) { + var gui = new SimpleGui(selectScreenType(elements.length), player, includePlayerSlots) { + @Override + public boolean onClick(int index, ClickType type, SlotActionType action, GuiElementInterface element) { + onClick.accept(this); + return super.onClick(index, type, action, element); + } + + @Override + public void onClose() { + onClose.accept(this); + } + }; + gui.setTitle(text); buildSelector(gui, elements); return gui; } - public static SimpleGui createSelectorGui(ServerPlayerEntity player, MutableText text, GuiElementInterface... elements) { - return createSelectorGui(player, text, false, elements); + public static SimpleGui createSelectorGui(ServerPlayerEntity player, MutableText text, Consumer onClick, Consumer onClose, GuiElementInterface... elements) { + return createSelectorGui(player, text, false, onClick, onClose, elements); + } + + public static SimpleGui createSelectorGui(ServerPlayerEntity player, MutableText text, Consumer onClick, Consumer onClose, Collection elements) { + return createSelectorGui(player, text, false, onClick, onClose, elements); } - public static SimpleGui createSelectorGui(ServerPlayerEntity player, MutableText text, Collection elements) { - return createSelectorGui(player, text, false, elements); + public static SimpleGui createSelectorGui(ServerPlayerEntity player, MutableText text, boolean includePlayerSlots, Consumer onClick, Consumer onClose, Collection elements) { + return createSelectorGui(player, text, includePlayerSlots, onClick, onClose, elements.toArray(new GuiElementInterface[0])); } public static SimpleGui createSelectorGui(ServerPlayerEntity player, MutableText text, boolean includePlayerSlots, Collection elements) { - return createSelectorGui(player, text, includePlayerSlots, elements.toArray(new GuiElementInterface[0])); + return createSelectorGui(player, text, includePlayerSlots, gui -> {}, gui -> {}, elements.toArray(new GuiElementInterface[0])); } public static Layer createSelectorLayer(int height, int width, Collection elements) { diff --git a/src/main/java/xyz/nucleoid/plasmid/impl/game/common/ui/ExtensionGuiElement.java b/src/main/java/xyz/nucleoid/plasmid/impl/game/common/ui/ExtensionGuiElement.java new file mode 100644 index 00000000..8aa12d3b --- /dev/null +++ b/src/main/java/xyz/nucleoid/plasmid/impl/game/common/ui/ExtensionGuiElement.java @@ -0,0 +1,39 @@ +package xyz.nucleoid.plasmid.impl.game.common.ui; + +import eu.pb4.sgui.api.elements.GuiElementInterface; +import eu.pb4.sgui.api.gui.SlotGuiInterface; +import net.minecraft.item.ItemStack; +import xyz.nucleoid.plasmid.api.game.common.ui.WaitingLobbyUiElement; +import xyz.nucleoid.plasmid.api.util.Guis; + +public record ExtensionGuiElement(GuiElementInterface delegate, WaitingLobbyUiLayoutEntry entry) implements GuiElementInterface { + @Override + public ItemStack getItemStack() { + return this.delegate.getItemStack(); + } + + @Override + public ClickCallback getGuiCallback() { + return (index, type, action, gui) -> { + if (WaitingLobbyUiElement.isClick(type, gui)) { + this.openExtendedGui(gui); + } + }; + } + + private void openExtendedGui(SlotGuiInterface parent) { + var player = parent.getPlayer(); + var name = this.delegate.getItemStackForDisplay(parent).getName().copy(); + + var ui = Guis.createSelectorGui(player, name, true, gui -> { + if (gui.isOpen()) { + // Refresh elements + this.openExtendedGui(parent); + } + }, gui -> { + parent.open(); + }, this.entry.getElement().createExtendedElements()); + + ui.open(); + } +} diff --git a/src/main/java/xyz/nucleoid/plasmid/impl/game/common/ui/WaitingLobbyUi.java b/src/main/java/xyz/nucleoid/plasmid/impl/game/common/ui/WaitingLobbyUi.java new file mode 100644 index 00000000..9c32a777 --- /dev/null +++ b/src/main/java/xyz/nucleoid/plasmid/impl/game/common/ui/WaitingLobbyUi.java @@ -0,0 +1,55 @@ +package xyz.nucleoid.plasmid.impl.game.common.ui; + +import eu.pb4.sgui.api.gui.HotbarGui; +import net.minecraft.network.packet.c2s.play.PlayerActionC2SPacket; +import net.minecraft.server.network.ServerPlayerEntity; +import net.minecraft.util.hit.BlockHitResult; +import net.minecraft.util.math.Direction; +import net.minecraft.util.math.Vec3d; +import xyz.nucleoid.plasmid.api.game.GameSpace; +import xyz.nucleoid.plasmid.api.game.common.ui.WaitingLobbyUiLayout; +import xyz.nucleoid.plasmid.api.game.event.GameWaitingLobbyEvents; +import xyz.nucleoid.stimuli.Stimuli; + +public class WaitingLobbyUi extends HotbarGui { + public WaitingLobbyUi(ServerPlayerEntity player, GameSpace gameSpace) { + super(player); + + var layout = WaitingLobbyUiLayout.of(elements -> { + int index = 0; + + for (var element : elements) { + this.setSlot(index, element); + index += 1; + } + }); + + try (var invokers = Stimuli.select().forEntity(player)) { + invokers.get(GameWaitingLobbyEvents.BUILD_UI_LAYOUT).onBuildUiLayout(layout, player); + } + + layout.refresh(); + } + + @Override + public boolean onHandSwing() { + super.onHandSwing(); + return true; + } + + @Override + public boolean onClickBlock(BlockHitResult hitResult) { + return true; + } + + @Override + public boolean onClickEntity(int entityId, EntityInteraction type, boolean isSneaking, Vec3d interactionPos) { + super.onClickEntity(entityId, type, isSneaking, interactionPos); + return true; + } + + @Override + public boolean canPlayerClose() { + return false; + } +} diff --git a/src/main/java/xyz/nucleoid/plasmid/impl/game/common/ui/WaitingLobbyUiLayoutEntry.java b/src/main/java/xyz/nucleoid/plasmid/impl/game/common/ui/WaitingLobbyUiLayoutEntry.java new file mode 100644 index 00000000..2aaf9ff0 --- /dev/null +++ b/src/main/java/xyz/nucleoid/plasmid/impl/game/common/ui/WaitingLobbyUiLayoutEntry.java @@ -0,0 +1,53 @@ +package xyz.nucleoid.plasmid.impl.game.common.ui; + +import eu.pb4.sgui.api.elements.GuiElementInterface; +import xyz.nucleoid.plasmid.api.game.common.ui.WaitingLobbyUiElement; + +import java.util.List; +import java.util.SequencedCollection; + +class WaitingLobbyUiLayoutEntry { + private final WaitingLobbyUiElement element; + + private SequencedCollection guiElements; + + protected WaitingLobbyUiLayoutEntry(WaitingLobbyUiElement element) { + this.element = element; + + this.guiElements = element.createExtendedElements(); + } + + public WaitingLobbyUiElement getElement() { + return this.element; + } + + public SequencedCollection getGuiElements() { + return this.guiElements; + } + + public void shrink() { + if (this.guiElements.size() > 1) { + var element = new ExtensionGuiElement(this.element.createMainElement(), this); + this.guiElements = List.of(element); + } + } + + public int size() { + return this.guiElements.size(); + } + + @Override + public String toString() { + return "WaitingLobbyUiLayoutEntry{element=" + element + ", guiElements=" + guiElements + "}"; + } + + protected static int getTotalSize(Iterable entries) { + int size = 0; + + for (var entry : entries) { + size += entry.size(); + } + + return size; + } +} diff --git a/src/main/java/xyz/nucleoid/plasmid/impl/game/common/ui/WaitingLobbyUiLayoutImpl.java b/src/main/java/xyz/nucleoid/plasmid/impl/game/common/ui/WaitingLobbyUiLayoutImpl.java new file mode 100644 index 00000000..cc941785 --- /dev/null +++ b/src/main/java/xyz/nucleoid/plasmid/impl/game/common/ui/WaitingLobbyUiLayoutImpl.java @@ -0,0 +1,114 @@ +package xyz.nucleoid.plasmid.impl.game.common.ui; + +import eu.pb4.sgui.api.elements.GuiElement; +import eu.pb4.sgui.api.elements.GuiElementInterface; +import xyz.nucleoid.plasmid.api.game.common.ui.WaitingLobbyUiElement; +import xyz.nucleoid.plasmid.api.game.common.ui.WaitingLobbyUiLayout; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Comparator; +import java.util.HashMap; +import java.util.List; +import java.util.Objects; +import java.util.function.Consumer; + +public final class WaitingLobbyUiLayoutImpl implements WaitingLobbyUiLayout { + private static final int SIZE = 9; + + private final Consumer callback; + + private final List leadingElements = new ArrayList<>(); + private final List trailingElements = new ArrayList<>(); + + public WaitingLobbyUiLayoutImpl(Consumer callback) { + this.callback = callback; + } + + @Override + public void addLeading(WaitingLobbyUiElement element) { + this.add(element, this.leadingElements); + } + + @Override + public void addTrailing(WaitingLobbyUiElement element) { + this.add(element, this.trailingElements); + } + + private void add(WaitingLobbyUiElement element, List elements) { + Objects.requireNonNull(element); + + if (this.leadingElements.contains(element) || this.trailingElements.contains(element)) { + throw new IllegalArgumentException("Element " + element + " has already been added to the layout"); + } else if (this.leadingElements.size() + this.trailingElements.size() >= SIZE) { + throw new IllegalStateException("Cannot have more than " + SIZE + " elements in the layout"); + } + + elements.add(element); + } + + private GuiElementInterface[] build() { + var elements = new GuiElementInterface[SIZE]; + Arrays.fill(elements, GuiElement.EMPTY); + + if (this.leadingElements.isEmpty() && this.trailingElements.isEmpty()) { + return elements; + } + + var elementsToEntries = new HashMap(this.leadingElements.size() + this.trailingElements.size()); + + for (var element : this.leadingElements) { + elementsToEntries.put(element, new WaitingLobbyUiLayoutEntry(element)); + } + + for (var element : this.trailingElements) { + elementsToEntries.put(element, new WaitingLobbyUiLayoutEntry(element)); + } + + var entries = new ArrayList<>(elementsToEntries.values()); + entries.sort(Comparator.comparingInt(WaitingLobbyUiLayoutEntry::size)); + + int shrinkIndex = 0; + + while (WaitingLobbyUiLayoutEntry.getTotalSize(entries) > SIZE) { + var entry = entries.get(shrinkIndex); + entry.shrink(); + + shrinkIndex += 1; + } + + int index = 0; + + for (var element : this.leadingElements) { + var entry = elementsToEntries.get(element); + + for (var guiElement : entry.getGuiElements()) { + elements[index] = guiElement; + index += 1; + } + } + + index = SIZE - 1; + + for (var element : this.trailingElements) { + var entry = elementsToEntries.get(element); + + for (var guiElement : entry.getGuiElements()) { + elements[index] = guiElement; + index -= 1; + } + } + + return elements; + } + + @Override + public void refresh() { + this.callback.accept(this.build()); + } + + @Override + public String toString() { + return "WaitingLobbyUiLayoutImpl{leadingElements=" + this.leadingElements + ", trailingElements=" + this.trailingElements + "}"; + } +} diff --git a/src/main/java/xyz/nucleoid/plasmid/impl/game/common/ui/element/LeaveGameWaitingLobbyUiElement.java b/src/main/java/xyz/nucleoid/plasmid/impl/game/common/ui/element/LeaveGameWaitingLobbyUiElement.java new file mode 100644 index 00000000..abc64ca6 --- /dev/null +++ b/src/main/java/xyz/nucleoid/plasmid/impl/game/common/ui/element/LeaveGameWaitingLobbyUiElement.java @@ -0,0 +1,31 @@ +package xyz.nucleoid.plasmid.impl.game.common.ui.element; + +import eu.pb4.sgui.api.elements.GuiElementBuilder; +import eu.pb4.sgui.api.elements.GuiElementInterface; +import net.minecraft.item.Items; +import net.minecraft.server.network.ServerPlayerEntity; +import net.minecraft.text.Text; +import xyz.nucleoid.plasmid.api.game.GameSpace; +import xyz.nucleoid.plasmid.api.game.common.ui.WaitingLobbyUiElement; + +public class LeaveGameWaitingLobbyUiElement implements WaitingLobbyUiElement { + private final GameSpace gameSpace; + private ServerPlayerEntity player; + + public LeaveGameWaitingLobbyUiElement(GameSpace gameSpace, ServerPlayerEntity player) { + this.gameSpace = gameSpace; + this.player = player; + } + + @Override + public GuiElementInterface createMainElement() { + return new GuiElementBuilder(Items.RED_BED) + .setItemName(Text.translatable("text.plasmid.game.waiting_lobby.leave_game")) + .setCallback((index, type, action, gui) -> { + if (WaitingLobbyUiElement.isClick(type, gui)) { + this.gameSpace.getPlayers().kick(this.player); + } + }) + .build(); + } +} diff --git a/src/main/java/xyz/nucleoid/plasmid/impl/game/common/ui/element/TeamSelectionWaitingLobbyUiElement.java b/src/main/java/xyz/nucleoid/plasmid/impl/game/common/ui/element/TeamSelectionWaitingLobbyUiElement.java new file mode 100644 index 00000000..3a34b2a1 --- /dev/null +++ b/src/main/java/xyz/nucleoid/plasmid/impl/game/common/ui/element/TeamSelectionWaitingLobbyUiElement.java @@ -0,0 +1,64 @@ +package xyz.nucleoid.plasmid.impl.game.common.ui.element; + +import eu.pb4.sgui.api.elements.GuiElementBuilder; +import eu.pb4.sgui.api.elements.GuiElementInterface; +import net.minecraft.item.Items; +import net.minecraft.text.Text; +import net.minecraft.util.Formatting; +import xyz.nucleoid.plasmid.api.game.common.team.GameTeamKey; +import xyz.nucleoid.plasmid.api.game.common.team.GameTeamList; +import xyz.nucleoid.plasmid.api.game.common.ui.WaitingLobbyUiElement; +import xyz.nucleoid.plasmid.api.util.ColoredBlocks; + +import java.util.ArrayList; +import java.util.SequencedCollection; +import java.util.function.Consumer; +import java.util.function.Predicate; + +public class TeamSelectionWaitingLobbyUiElement implements WaitingLobbyUiElement { + private final GameTeamList teams; + + private final Predicate activePredicate; + private final Consumer selectCallback; + + public TeamSelectionWaitingLobbyUiElement(GameTeamList teams, Predicate activePredicate, Consumer selectCallback) { + this.teams = teams; + + this.activePredicate = activePredicate; + this.selectCallback = selectCallback; + } + + @Override + public GuiElementInterface createMainElement() { + return new GuiElementBuilder(Items.PAPER) + .setItemName(Text.translatable("text.plasmid.team_selection.teams")) + .build(); + } + + @Override + public SequencedCollection createExtendedElements() { + var extendedElements = new ArrayList(this.teams.list().size()); + + for (var team : this.teams) { + var key = team.key(); + var config = team.config(); + + var name = Text.translatable("text.plasmid.team_selection.request_team", config.name()) + .formatted(Formatting.BOLD, config.chatFormatting()); + + var element = new GuiElementBuilder(ColoredBlocks.wool(config.blockDyeColor()).asItem()) + .setItemName(name) + .setCallback((index, type, action, gui) -> { + if (WaitingLobbyUiElement.isClick(type, gui)) { + this.selectCallback.accept(key); + } + }) + .glow(this.activePredicate.test(key)) + .build(); + + extendedElements.add(element); + } + + return extendedElements; + } +} diff --git a/src/main/resources/data/plasmid/lang/en_us.json b/src/main/resources/data/plasmid/lang/en_us.json index 033bfddf..d0245fb9 100644 --- a/src/main/resources/data/plasmid/lang/en_us.json +++ b/src/main/resources/data/plasmid/lang/en_us.json @@ -58,6 +58,7 @@ "text.plasmid.game.waiting_lobby.bar.cancel": "Game start cancelled! ", "text.plasmid.game.waiting_lobby.bar.countdown": "Starting in %s seconds!", "text.plasmid.game.waiting_lobby.bar.waiting": "Waiting for players...", + "text.plasmid.game.waiting_lobby.leave_game": "Leave Game", "text.plasmid.game.waiting_lobby.sidebar.players": "Players: %s%s%s", "text.plasmid.join_result.already_joined": "You have already joined this game!", "text.plasmid.join_result.error": "An unexpected error occurred while adding you to this game!", @@ -127,6 +128,7 @@ "text.plasmid.team_selection.request_team": "Request %s Team", "text.plasmid.team_selection.requested_team": "You have requested to join the %s", "text.plasmid.team_selection.suffixed_team": "%s Team", + "text.plasmid.team_selection.teams": "Teams", "text.plasmid.test": "Test", "text.plasmid.ui.game_join.invalid.description": "No game or menu is linked to this entry", "text.plasmid.ui.game_join.invalid.name": "Invalid Menu Entry", diff --git a/src/test/java/xyz/nucleoid/plasmid/test/StaticWaitingLobbyUiElement.java b/src/test/java/xyz/nucleoid/plasmid/test/StaticWaitingLobbyUiElement.java new file mode 100644 index 00000000..2735efe1 --- /dev/null +++ b/src/test/java/xyz/nucleoid/plasmid/test/StaticWaitingLobbyUiElement.java @@ -0,0 +1,23 @@ +package xyz.nucleoid.plasmid.test; + +import eu.pb4.sgui.api.elements.GuiElementInterface; +import xyz.nucleoid.plasmid.api.game.common.ui.WaitingLobbyUiElement; + +import java.util.List; +import java.util.SequencedCollection; + +public record StaticWaitingLobbyUiElement(GuiElementInterface mainElement, SequencedCollection extendedElements) implements WaitingLobbyUiElement { + public StaticWaitingLobbyUiElement(GuiElementInterface element) { + this(element, List.of(element)); + } + + @Override + public GuiElementInterface createMainElement() { + return this.mainElement; + } + + @Override + public SequencedCollection createExtendedElements() { + return this.extendedElements; + } +} diff --git a/src/test/java/xyz/nucleoid/plasmid/test/WaitingLobbyUiLayoutTests.java b/src/test/java/xyz/nucleoid/plasmid/test/WaitingLobbyUiLayoutTests.java new file mode 100644 index 00000000..5e9d5327 --- /dev/null +++ b/src/test/java/xyz/nucleoid/plasmid/test/WaitingLobbyUiLayoutTests.java @@ -0,0 +1,236 @@ +package xyz.nucleoid.plasmid.test; + +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; +import eu.pb4.sgui.api.elements.GuiElement; +import eu.pb4.sgui.api.elements.GuiElementBuilder; +import eu.pb4.sgui.api.elements.GuiElementInterface; +import it.unimi.dsi.fastutil.chars.Char2ObjectMap; +import it.unimi.dsi.fastutil.chars.Char2ObjectOpenHashMap; +import net.minecraft.Bootstrap; +import net.minecraft.SharedConstants; +import net.minecraft.item.Items; +import net.minecraft.text.Text; +import xyz.nucleoid.plasmid.api.game.common.ui.WaitingLobbyUiLayout; +import xyz.nucleoid.plasmid.impl.game.common.ui.ExtensionGuiElement; +import xyz.nucleoid.plasmid.impl.game.common.ui.WaitingLobbyUiLayoutImpl; + +import java.util.List; +import java.util.function.BiConsumer; + +import static org.junit.jupiter.api.Assertions.*; + +public class WaitingLobbyUiLayoutTests { + @BeforeAll + public static void beforeAll() { + SharedConstants.createGameVersion(); + Bootstrap.initialize(); + } + + @Test + public void emptyLayout() { + expectBuiltLayout((layout, map) -> {}, " "); + } + + @Test + public void simpleLeadingLayout() { + expectBuiltLayout((layout, map) -> { + layout.addLeading(new StaticWaitingLobbyUiElement(map.get('A'))); + }, "A "); + } + + @Test + public void simpleTrailingLayout() { + expectBuiltLayout((layout, map) -> { + layout.addTrailing(new StaticWaitingLobbyUiElement(map.get('A'))); + }, " A"); + } + + @Test + public void simpleBothLayout() { + expectBuiltLayout((layout, map) -> { + layout.addLeading(new StaticWaitingLobbyUiElement(map.get('A'))); + layout.addTrailing(new StaticWaitingLobbyUiElement(map.get('B'))); + }, "A B"); + } + + @Test + public void extendedLeadingLayout() { + expectBuiltLayout((layout, map) -> { + layout.addLeading(new StaticWaitingLobbyUiElement(map.get('E'), List.of( + map.get('A'), + map.get('B'), + map.get('C'), + map.get('D') + ))); + }, "ABCD "); + } + + @Test + public void extendedTrailingLayout() { + expectBuiltLayout((layout, map) -> { + layout.addTrailing(new StaticWaitingLobbyUiElement(map.get('E'), List.of( + map.get('A'), + map.get('B'), + map.get('C'), + map.get('D') + ))); + }, " DCBA"); + } + + @Test + public void extendedBothLayout() { + expectBuiltLayout((layout, map) -> { + layout.addLeading(new StaticWaitingLobbyUiElement(map.get('E'), List.of( + map.get('A'), + map.get('B'), + map.get('C'), + map.get('D') + ))); + + layout.addTrailing(new StaticWaitingLobbyUiElement(map.get('J'), List.of( + map.get('F'), + map.get('G'), + map.get('H'), + map.get('I') + ))); + }, "ABCD IHGF"); + } + + @Test + public void packedLayout() { + expectBuiltLayout((layout, map) -> { + layout.addLeading(new StaticWaitingLobbyUiElement(map.get('D'), List.of( + map.get('A'), + map.get('B'), + map.get('C') + ))); + + layout.addTrailing(new StaticWaitingLobbyUiElement(map.get('K'), List.of( + map.get('E'), + map.get('F'), + map.get('G'), + map.get('H'), + map.get('I'), + map.get('J') + ))); + }, "ABCJIHGFE"); + } + + @Test + public void shrunkLayout() { + expectBuiltLayout((layout, map) -> { + layout.addLeading(new StaticWaitingLobbyUiElement(map.get('D'), List.of( + map.get('A'), + map.get('B'), + map.get('C') + ))); + + layout.addTrailing(new StaticWaitingLobbyUiElement(map.get('L'), List.of( + map.get('E'), + map.get('F'), + map.get('G'), + map.get('H'), + map.get('I'), + map.get('J'), + map.get('K') + ))); + }, "D KJIHGFE"); + } + + @Test + public void cannotAddDuplicateElement() { + assertThrows(IllegalArgumentException.class, () -> { + var layout = createNonBuildingLayout(); + + var guiElement = new GuiElementBuilder(Items.MELON).build(); + var element = new StaticWaitingLobbyUiElement(guiElement); + + layout.addLeading(element); + layout.addLeading(element); + }); + } + + @Test + public void cannotAddDuplicateElementToEitherSide() { + assertThrows(IllegalArgumentException.class, () -> { + var layout = createNonBuildingLayout(); + + var guiElement = new GuiElementBuilder(Items.PUMPKIN).build(); + var element = new StaticWaitingLobbyUiElement(guiElement); + + layout.addLeading(element); + layout.addTrailing(element); + }); + } + + @Test + public void cannotExceedMaximumElements() { + assertThrows(IllegalStateException.class, () -> { + var layout = createNonBuildingLayout(); + + for (int i = 0; i < 10; i++) { + var guiElement = new GuiElementBuilder(Items.DIRT).build(); + var element = new StaticWaitingLobbyUiElement(guiElement); + + if (i % 2 == 0) { + layout.addLeading(element); + } else { + layout.addTrailing(element); + } + } + }); + } + + private static void expectBuiltLayout(BiConsumer> consumer, String expectedPattern) { + var map = createGuiElementMap(); + var expected = buildGuiElementsFromPattern(expectedPattern, map); + + var layout = WaitingLobbyUiLayout.of(actual -> { + extractExtensionGuiElements(actual); + assertArrayEquals(expected, actual); + }); + + consumer.accept(layout, map); + layout.refresh(); + } + + private static Char2ObjectMap createGuiElementMap() { + var map = new Char2ObjectOpenHashMap(); + + for (char c = 'A'; c <= 'Z'; c++) { + var element = new GuiElementBuilder(Items.PAPER) + .setName(Text.literal("" + c)) + .build(); + + map.put(c, element); + } + + return map; + } + + private static GuiElementInterface[] buildGuiElementsFromPattern(String pattern, Char2ObjectMap map) { + var array = new GuiElementInterface[pattern.length()]; + + for (int i = 0; i < pattern.length(); i++) { + char c = pattern.charAt(i); + array[i] = c == ' ' ? GuiElement.EMPTY : map.get(c); + } + + return array; + } + + private static void extractExtensionGuiElements(GuiElementInterface[] elements) { + for (int index = 0; index < elements.length; index++) { + if (elements[index] instanceof ExtensionGuiElement element) { + elements[index] = element.delegate(); + } + } + } + + private static WaitingLobbyUiLayout createNonBuildingLayout() { + // WaitingLobbyUiLayout#refresh isn't expected to be called, + // so the callback can be null + return new WaitingLobbyUiLayoutImpl(null); + } +} diff --git a/src/testmod/java/xyz/nucleoid/plasmid/test/TestConfig.java b/src/testmod/java/xyz/nucleoid/plasmid/test/TestConfig.java index 5ab946f8..ffd00989 100644 --- a/src/testmod/java/xyz/nucleoid/plasmid/test/TestConfig.java +++ b/src/testmod/java/xyz/nucleoid/plasmid/test/TestConfig.java @@ -12,10 +12,11 @@ import java.util.Optional; -public record TestConfig(int integer, BlockState state, Optional> items) { +public record TestConfig(int integer, BlockState state, Optional> items, int teamCount) { public static final MapCodec CODEC = RecordCodecBuilder.mapCodec(i -> i.group( Codec.INT.optionalFieldOf("integer", 0).forGetter(TestConfig::integer), BlockState.CODEC.optionalFieldOf("state", Blocks.BLUE_STAINED_GLASS.getDefaultState()).forGetter(TestConfig::state), - RegistryCodecs.entryList(RegistryKeys.ITEM).optionalFieldOf("items").forGetter(TestConfig::items) + RegistryCodecs.entryList(RegistryKeys.ITEM).optionalFieldOf("items").forGetter(TestConfig::items), + Codec.INT.optionalFieldOf("team_count", 0).forGetter(TestConfig::teamCount) ).apply(i, TestConfig::new)); } diff --git a/src/testmod/java/xyz/nucleoid/plasmid/test/TestGame.java b/src/testmod/java/xyz/nucleoid/plasmid/test/TestGame.java index f54bf23c..ab4d6001 100644 --- a/src/testmod/java/xyz/nucleoid/plasmid/test/TestGame.java +++ b/src/testmod/java/xyz/nucleoid/plasmid/test/TestGame.java @@ -2,7 +2,10 @@ import net.minecraft.block.Block; import net.minecraft.block.BlockState; +import net.minecraft.block.Blocks; +import net.minecraft.block.ButtonBlock; import net.minecraft.block.LeavesBlock; +import net.minecraft.block.enums.BlockFace; import net.minecraft.item.Item; import net.minecraft.item.Items; import net.minecraft.scoreboard.AbstractTeam; @@ -10,7 +13,10 @@ import net.minecraft.server.network.ServerPlayerEntity; import net.minecraft.text.Style; import net.minecraft.text.Text; +import net.minecraft.util.ActionResult; +import net.minecraft.util.DyeColor; import net.minecraft.util.Identifier; +import net.minecraft.util.Util; import net.minecraft.util.math.BlockPos; import net.minecraft.util.math.Vec3d; import net.minecraft.world.GameMode; @@ -20,6 +26,7 @@ import xyz.nucleoid.map_templates.MapTemplate; import xyz.nucleoid.plasmid.api.game.*; import xyz.nucleoid.plasmid.api.game.common.team.*; +import xyz.nucleoid.plasmid.api.game.common.team.GameTeamConfig.Colors; import xyz.nucleoid.plasmid.impl.Plasmid; import xyz.nucleoid.plasmid.api.game.common.GameWaitingLobby; import xyz.nucleoid.plasmid.api.game.common.GlobalWidgets; @@ -33,13 +40,16 @@ import xyz.nucleoid.plasmid.api.game.world.generator.TemplateChunkGenerator; import xyz.nucleoid.plasmid.api.util.WoodType; import xyz.nucleoid.stimuli.event.EventResult; +import xyz.nucleoid.stimuli.event.block.BlockUseEvent; import xyz.nucleoid.stimuli.event.player.PlayerDeathEvent; import java.lang.reflect.Method; +import java.util.ArrayList; import java.util.Arrays; import java.util.List; public final class TestGame { + private static final BlockState BUTTON = Blocks.OAK_BUTTON.getDefaultState().with(ButtonBlock.FACE, BlockFace.FLOOR); private static final List WOOD_TYPE_BLOCK_FIELDS = Arrays.stream(WoodType.class.getMethods()).filter(x -> x.getReturnType() == Block.class).toList(); private static final StatisticKey TEST_KEY = StatisticKey.doubleKey(Identifier.of(Plasmid.ID, "test")); @@ -59,6 +69,8 @@ public static GameOpenProcedure open(GameOpenContext context) { .setGameRule(GameRules.KEEP_INVENTORY, true); return context.openWithWorld(worldConfig, (activity, world) -> { + var gameSpace = activity.getGameSpace(); + activity.listen(GamePlayerEvents.OFFER, JoinOffer::accept); activity.listen(GamePlayerEvents.ACCEPT, acceptor -> acceptor.teleport(world, new Vec3d(0.0, 65.0, 0.0)) @@ -69,16 +81,64 @@ public static GameOpenProcedure open(GameOpenContext context) { GameWaitingLobby.addTo(activity, new WaitingLobbyConfig(1, 99)); + int teamCount = context.config().teamCount(); + + if (teamCount > 0) { + var random = world.getRandom(); + var teams = new ArrayList(); + + for (int i = 0; i < teamCount; i++) { + var dyeColor = Util.getRandom(DyeColor.values(), random); + var color = Colors.from(dyeColor); + + var name = Text.literal(""); + + var key = new GameTeamKey("team_" + i); + + var config = GameTeamConfig.builder() + .setName(name) + .setColors(color) + .build(); + + teams.add(new GameTeam(key, config)); + } + + TeamSelectionLobby.addTo(activity, new GameTeamList(teams)); + } + activity.allow(GameRuleType.PVP).allow(GameRuleType.MODIFY_ARMOR); activity.deny(GameRuleType.FALL_DAMAGE).deny(GameRuleType.HUNGER); - activity.deny(GameRuleType.THROW_ITEMS).deny(GameRuleType.MODIFY_INVENTORY); + activity.deny(GameRuleType.THROW_ITEMS); + + // Waiting lobbies disable interaction, so the rule must be re-enabled for the event to be invoked + activity.allow(GameRuleType.INTERACTION); + + activity.listen(BlockUseEvent.EVENT, (player, hand, hitResult) -> { + var state = player.getWorld().getBlockState(hitResult.getBlockPos()); + + if (state == BUTTON) { + // These should be mutually exclusive + boolean spectator = gameSpace.getPlayers().spectators().contains(player); + boolean participant = gameSpace.getPlayers().participants().contains(player); + + if (spectator && participant) { + player.sendMessage(Text.empty().append(player.getDisplayName()).append(" is both a spectator and participant... somehow...")); + } else if (spectator) { + player.sendMessage(Text.empty().append(player.getDisplayName()).append(" is a spectator")); + } else if (participant) { + player.sendMessage(Text.empty().append(player.getDisplayName()).append(" is a participant")); + } + } + + return ActionResult.PASS; + }); activity.listen(PlayerDeathEvent.EVENT, (player, source) -> { player.setPos(0.0, 65.0, 0.0); return EventResult.DENY; }); - activity.listen(GameActivityEvents.REQUEST_START, () -> startGame(activity.getGameSpace())); + activity.listen(GameActivityEvents.REQUEST_START, () -> startGame(gameSpace)); }); } @@ -92,7 +152,7 @@ private static GameResult startGame(GameSpace gameSpace) { long currentTime = gameSpace.getTime(); activity.deny(GameRuleType.PVP).allow(GameRuleType.MODIFY_ARMOR); activity.deny(GameRuleType.FALL_DAMAGE).deny(GameRuleType.HUNGER); - activity.deny(GameRuleType.THROW_ITEMS).deny(GameRuleType.MODIFY_INVENTORY); + activity.deny(GameRuleType.THROW_ITEMS); activity.deny(GameRuleType.INTERACTION).allow(GameRuleType.USE_BLOCKS); @@ -148,7 +208,13 @@ private static GameResult startGame(GameSpace gameSpace) { private static MapTemplate generateMapTemplate(BlockState state) { var template = MapTemplate.createEmpty(); - for (var pos : BlockBounds.of(-5, 64, -5, 5, 64, 5)) { + var bounds = BlockBounds.of(-5, 64, -5, 5, 64, 5); + var max = bounds.max(); + + var edge = new BlockPos(max.getX(), max.getY() + 1, max.getZ()); + template.setBlockState(edge, BUTTON); + + for (var pos : bounds) { template.setBlockState(pos, state); } diff --git a/src/testmod/resources/data/testmod/plasmid/game/test_four_teams.json b/src/testmod/resources/data/testmod/plasmid/game/test_four_teams.json new file mode 100644 index 00000000..b3136147 --- /dev/null +++ b/src/testmod/resources/data/testmod/plasmid/game/test_four_teams.json @@ -0,0 +1,10 @@ +{ + "type": "testmod:test", + "icon": "diamond", + "description": [ + "Lorem ipsum minigamum", + "Example description" + ], + "items": "#minecraft:slabs", + "team_count": 4 +} diff --git a/src/testmod/resources/data/testmod/plasmid/game/test_one_team.json b/src/testmod/resources/data/testmod/plasmid/game/test_one_team.json new file mode 100644 index 00000000..4133e8bc --- /dev/null +++ b/src/testmod/resources/data/testmod/plasmid/game/test_one_team.json @@ -0,0 +1,10 @@ +{ + "type": "testmod:test", + "icon": "diamond", + "description": [ + "Lorem ipsum minigamum", + "Example description" + ], + "items": "#minecraft:slabs", + "team_count": 1 +} diff --git a/src/testmod/resources/data/testmod/plasmid/game/test_sixteen_teams.json b/src/testmod/resources/data/testmod/plasmid/game/test_sixteen_teams.json new file mode 100644 index 00000000..d65638dc --- /dev/null +++ b/src/testmod/resources/data/testmod/plasmid/game/test_sixteen_teams.json @@ -0,0 +1,10 @@ +{ + "type": "testmod:test", + "icon": "diamond", + "description": [ + "Lorem ipsum minigamum", + "Example description" + ], + "items": "#minecraft:slabs", + "team_count": 16 +}