Skip to content

Commit

Permalink
Overhaul the waiting lobby user interface (#303)
Browse files Browse the repository at this point in the history
* Add an event for building waiting lobby user interface layouts

* Indicate the requested team in the waiting lobby user interface

* Allow accessing extended user interfaces from the waiting lobby user interface

* Allow leaving games from the waiting lobby user interface

* Fix single extended elements shrinking in waiting lobby user interfaces

* Use direction-independent terms for waiting lobby user interface element alignments

* Prevent spectators from selecting teams in the waiting lobby user interface

* Fix the waiting lobby user interface blocking world interactions
  • Loading branch information
haykam821 authored Nov 15, 2024
1 parent 4f417c9 commit f91f40b
Show file tree
Hide file tree
Showing 20 changed files with 859 additions and 60 deletions.
Original file line number Diff line number Diff line change
@@ -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;
Expand All @@ -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;

Expand Down Expand Up @@ -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);

Expand Down Expand Up @@ -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);
}
}
}
}
Expand All @@ -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) {
Expand Down
Original file line number Diff line number Diff line change
@@ -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;
Expand All @@ -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<GameTeamKey> maxTeamSize = new Reference2IntOpenHashMap<>();
private final Map<UUID, GameTeamKey> teamPreference = new Object2ObjectOpenHashMap<>();

private TeamSelectionLobby(GameTeamList teams) {
private TeamSelectionLobby(GameSpace gameSpace, GameTeamList teams) {
this.gameSpace = gameSpace;
this.teams = teams;
}

Expand All @@ -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;
}
Expand All @@ -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;
}));
}

/**
Expand Down
Original file line number Diff line number Diff line change
@@ -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<GuiElementInterface> createExtendedElements() {
return List.of(this.createMainElement());
}

static boolean isClick(ClickType type, SlotGuiInterface gui) {
return type.isRight || !(gui instanceof HotbarGui);
}
}
Original file line number Diff line number Diff line change
@@ -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<GuiElementInterface[]> callback) {
return new WaitingLobbyUiLayoutImpl(Objects.requireNonNull(callback));
}
}
Original file line number Diff line number Diff line change
@@ -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.
* <p>
* 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<BuildUiLayout> 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);
}
}
33 changes: 26 additions & 7 deletions src/main/java/xyz/nucleoid/plasmid/api/util/Guis.java
Original file line number Diff line number Diff line change
@@ -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;
Expand All @@ -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;
Expand All @@ -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<SimpleGui> onClick, Consumer<SimpleGui> 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<SimpleGui> onClick, Consumer<SimpleGui> onClose, GuiElementInterface... elements) {
return createSelectorGui(player, text, false, onClick, onClose, elements);
}

public static SimpleGui createSelectorGui(ServerPlayerEntity player, MutableText text, Consumer<SimpleGui> onClick, Consumer<SimpleGui> onClose, Collection<GuiElementInterface> elements) {
return createSelectorGui(player, text, false, onClick, onClose, elements);
}

public static SimpleGui createSelectorGui(ServerPlayerEntity player, MutableText text, Collection<GuiElementInterface> elements) {
return createSelectorGui(player, text, false, elements);
public static SimpleGui createSelectorGui(ServerPlayerEntity player, MutableText text, boolean includePlayerSlots, Consumer<SimpleGui> onClick, Consumer<SimpleGui> onClose, Collection<GuiElementInterface> elements) {
return createSelectorGui(player, text, includePlayerSlots, onClick, onClose, elements.toArray(new GuiElementInterface[0]));
}

public static SimpleGui createSelectorGui(ServerPlayerEntity player, MutableText text, boolean includePlayerSlots, Collection<GuiElementInterface> 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<GuiElementInterface> elements) {
Expand Down
Original file line number Diff line number Diff line change
@@ -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();
}
}
Loading

0 comments on commit f91f40b

Please sign in to comment.