diff --git a/resources/assets/edenmod/textures/gui/icons/festival.png b/resources/assets/edenmod/textures/gui/icons/festival.png new file mode 100644 index 0000000..97005d7 Binary files /dev/null and b/resources/assets/edenmod/textures/gui/icons/festival.png differ diff --git a/resources/assets/edenmod/textures/gui/icons/nol.png b/resources/assets/edenmod/textures/gui/icons/nol.png index 00db353..6f1a671 100644 Binary files a/resources/assets/edenmod/textures/gui/icons/nol.png and b/resources/assets/edenmod/textures/gui/icons/nol.png differ diff --git a/resources/assets/edenmod/textures/gui/icons/notg.png b/resources/assets/edenmod/textures/gui/icons/notg.png index 0bf34ed..83707f0 100644 Binary files a/resources/assets/edenmod/textures/gui/icons/notg.png and b/resources/assets/edenmod/textures/gui/icons/notg.png differ diff --git a/resources/assets/edenmod/textures/gui/icons/tcc.png b/resources/assets/edenmod/textures/gui/icons/tcc.png index c2324d4..26275e4 100644 Binary files a/resources/assets/edenmod/textures/gui/icons/tcc.png and b/resources/assets/edenmod/textures/gui/icons/tcc.png differ diff --git a/resources/assets/edenmod/textures/gui/icons/tna.png b/resources/assets/edenmod/textures/gui/icons/tna.png index c1fcc84..d914b7e 100644 Binary files a/resources/assets/edenmod/textures/gui/icons/tna.png and b/resources/assets/edenmod/textures/gui/icons/tna.png differ diff --git a/resources/assets/edenmod/textures/gui/icons/wtp.png b/resources/assets/edenmod/textures/gui/icons/wtp.png index 4be4d06..34796fa 100644 Binary files a/resources/assets/edenmod/textures/gui/icons/wtp.png and b/resources/assets/edenmod/textures/gui/icons/wtp.png differ diff --git a/src/tel/eden/mod/EdenModClient.java b/src/tel/eden/mod/EdenModClient.java index 4775d4c..0277127 100644 --- a/src/tel/eden/mod/EdenModClient.java +++ b/src/tel/eden/mod/EdenModClient.java @@ -414,6 +414,35 @@ public void openCenteredEmotePicker() { }); } + private volatile String currentWynnWorld = ""; + private volatile boolean festivalEnabled = false; + private volatile boolean annihilationActive = false; + + public String currentWynnWorld() { + return currentWynnWorld; + } + + public boolean isFestivalEnabled() { + return festivalEnabled; + } + + public boolean isAnnihilationActive() { + return annihilationActive; + } + + public PartyInfo activeParty() { + String ign = playerName(); + if (ign == null || ign.isBlank()) { + return null; + } + for (PartyInfo party : knownParties) { + if (party.host().equalsIgnoreCase(ign) || party.members().stream().anyMatch(m -> m.equalsIgnoreCase(ign))) { + return party; + } + } + return null; + } + /** An immutable snapshot of the currently known parties, safe to read off-thread. */ public java.util.List knownParties() { return knownParties; @@ -1134,6 +1163,12 @@ public void onPartyUpdate(String event, String actor, PartyInfo party, String co } } + @Override + public void onPartyConfig(boolean festivalEnabled, boolean annihilationActive) { + EdenModClient.this.festivalEnabled = festivalEnabled; + EdenModClient.this.annihilationActive = annihilationActive; + } + @Override public void onPartyList(java.util.List parties, String color) { replaceKnownParties(parties); @@ -2588,14 +2623,27 @@ private void setInGameWorld(boolean active) { /** Returns true when the Wynncraft tab list is visible (game world or /class screen). */ private boolean checkWynncraftTabActive(Minecraft mc) { net.minecraft.client.multiplayer.ClientPacketListener conn = mc.getConnection(); - if (conn == null) + if (conn == null) { + currentWynnWorld = ""; return false; + } for (net.minecraft.client.multiplayer.PlayerInfo info : conn.getOnlinePlayers()) { net.minecraft.network.chat.Component name = info.getTabListDisplayName(); - if (name != null && (name.getString().contains("Global [") || name.getString().contains("Island Info"))) { - return true; + if (name != null) { + String str = name.getString(); + if (str.contains("Global [") || str.contains("Island Info")) { + if (str.contains("Global [")) { + int start = str.indexOf("Global [") + 8; + int end = str.indexOf(']', start); + if (end > start) { + currentWynnWorld = str.substring(start, end).trim(); + } + } + return true; + } } } + currentWynnWorld = ""; return false; } @@ -2690,6 +2738,7 @@ private synchronized void disconnect() { partyCommandGeneration.incrementAndGet(); onWynncraft = false; loginPending = false; + currentWynnWorld = ""; bridgeStatus = BridgeStatus.UNKNOWN; liveGuildRank = ""; liveDiscordRank = ""; @@ -2854,6 +2903,7 @@ public void handleSystemChat(Component message) { // Annihilation world-event warning (the only advance timing signal we get). java.util.OptionalInt anni = AnnihilationParser.parse(message); if (anni.isPresent()) { + annihilationActive = true; int seconds = anni.getAsInt(); if (annihilationRelay.shouldSend(new CapturedMessage("annihilation", null, String.valueOf(seconds)))) { current.sendAnnihilation(seconds); diff --git a/src/tel/eden/mod/chat/PartyFormatter.java b/src/tel/eden/mod/chat/PartyFormatter.java index 81c3336..00f552d 100644 --- a/src/tel/eden/mod/chat/PartyFormatter.java +++ b/src/tel/eden/mod/chat/PartyFormatter.java @@ -22,7 +22,17 @@ private PartyFormatter() { /** " wants to raid (n/max) [JOIN #id]" with the open note, if any. */ public static Component partyOpen(PartyInfo party) { - MutableComponent body = Component.empty().append(Component.literal(party.host()).withStyle(ChatFormatting.AQUA)).append(Component.literal(" wants to raid ").withStyle(ChatFormatting.GREEN)).append(Component.literal(party.raid()).withStyle(ChatFormatting.YELLOW)).append(Component.literal(" (" + party.size() + "/" + party.max() + ") ").withStyle(ChatFormatting.GRAY)).append(joinButton(party.id())); + MutableComponent body = Component.empty(); + if (party.ally()) { + body.append(Component.literal("[Ally] ").withStyle(ChatFormatting.LIGHT_PURPLE)); + } + String action = "Festival Minigames".equalsIgnoreCase(party.raid()) ? " wants to play " : " wants to raid "; + Style countStyle = Style.EMPTY.withColor(ChatFormatting.GRAY).withHoverEvent(new HoverEvent.ShowText(playerListHover(party))); + body.append(Component.literal(party.host()).withStyle(ChatFormatting.AQUA)).append(Component.literal(action).withStyle(ChatFormatting.GREEN)).append(Component.literal(party.raid()).withStyle(ChatFormatting.YELLOW)).append(Component.literal(" (" + party.size() + "/" + party.max() + ") ").setStyle(countStyle)); + if (party.world() != null && !party.world().isBlank()) { + body.append(Component.literal("[" + party.world() + "] ").withStyle(ChatFormatting.DARK_AQUA)); + } + body.append(joinButton(party.id())); if (!party.note().isBlank()) { body.append(Component.literal(" — ").withStyle(ChatFormatting.GRAY)).append(Component.literal("\"" + party.note() + "\"").withStyle(Style.EMPTY.withColor(ChatFormatting.WHITE).withItalic(true))); } @@ -43,7 +53,12 @@ public static Component partyLeave(String actor, PartyInfo party) { /** "Party #id () is full! [Create party]" (the button shows for the host). */ public static Component partyFull(PartyInfo party) { - MutableComponent body = Component.empty().append(Component.literal("Party #" + party.id() + " ").withStyle(ChatFormatting.GOLD)).append(Component.literal("(" + party.raid() + ") ").withStyle(ChatFormatting.YELLOW)).append(Component.literal("is full! ").withStyle(ChatFormatting.GREEN)).append(Component.literal(String.join(", ", party.members())).withStyle(ChatFormatting.GRAY)); + MutableComponent body = Component.empty(); + if (party.ally()) { + body.append(Component.literal("[Ally] ").withStyle(ChatFormatting.LIGHT_PURPLE)); + } + List realMembers = party.members().stream().filter(m -> !m.equals("*filled*")).toList(); + body.append(Component.literal("Party #" + party.id() + " ").withStyle(ChatFormatting.GOLD)).append(Component.literal("(" + party.raid() + ") ").withStyle(ChatFormatting.YELLOW)).append(Component.literal("is full! ").withStyle(ChatFormatting.GREEN)).append(Component.literal(String.join(", ", realMembers)).withStyle(ChatFormatting.GRAY)); // Offer the host a one-click "make the in-game party" prompt. String self = localName(); if (self != null && self.equalsIgnoreCase(party.host()) && !party.members().isEmpty()) { @@ -54,7 +69,8 @@ public static Component partyFull(PartyInfo party) { /** Clickable that runs {@code /eden party makeingame } to form the in-game party. */ private static Component createButton(PartyInfo party) { - Style style = Style.EMPTY.withColor(ChatFormatting.GOLD).withUnderlined(true).withClickEvent(new ClickEvent.RunCommand("/eden party makeingame " + String.join(" ", party.members()))).withHoverEvent(new HoverEvent.ShowText(Component.literal("Click to create the in-game party and invite everyone"))); + List realMembers = party.members().stream().filter(m -> !m.equals("*filled*")).toList(); + Style style = Style.EMPTY.withColor(ChatFormatting.GOLD).withUnderlined(true).withClickEvent(new ClickEvent.RunCommand("/eden party makeingame " + String.join(" ", realMembers))).withHoverEvent(new HoverEvent.ShowText(Component.literal("Click to create the in-game party and invite everyone"))); return Component.literal("[Create party]").setStyle(style); } @@ -71,8 +87,17 @@ public static Component listing(List parties) { MutableComponent out = line(Component.literal("Open Eden parties (" + parties.size() + "):").withStyle(ChatFormatting.GREEN)); for (PartyInfo party : parties) { out.append("\n"); + if (party.ally()) { + out.append(Component.literal("[Ally] ").withStyle(ChatFormatting.LIGHT_PURPLE)); + } else { + out.append(Component.literal("[Guild] ").withStyle(ChatFormatting.DARK_GREEN)); + } out.append(Component.literal(" " + party.raid() + " ").withStyle(ChatFormatting.YELLOW)); - out.append(Component.literal(party.size() + "/" + party.max() + " | ").withStyle(ChatFormatting.GRAY)); + Style countStyle = Style.EMPTY.withColor(ChatFormatting.GRAY).withHoverEvent(new HoverEvent.ShowText(playerListHover(party))); + out.append(Component.literal(party.size() + "/" + party.max() + " | ").setStyle(countStyle)); + if (party.world() != null && !party.world().isBlank()) { + out.append(Component.literal("[" + party.world() + "] ").withStyle(ChatFormatting.DARK_AQUA)); + } out.append(Component.literal(party.host() + " ").withStyle(ChatFormatting.AQUA)); out.append(joinButton(party.id())); if (!party.note().isBlank()) { @@ -95,4 +120,26 @@ private static Component joinButton(int id) { Style style = Style.EMPTY.withColor(ChatFormatting.GREEN).withUnderlined(true).withClickEvent(new ClickEvent.RunCommand("/eden party join " + id)).withHoverEvent(new HoverEvent.ShowText(Component.literal("Click to join party #" + id))); return Component.literal("[JOIN #" + id + "]").setStyle(style); } + + private static Component playerListHover(PartyInfo party) { + MutableComponent tooltip = Component.literal("Party Members (" + party.size() + "/" + party.max() + "):").withStyle(ChatFormatting.GOLD); + for (int i = 0; i < party.size(); i++) { + tooltip.append("\n"); + String raw = party.members().get(i); + if ("*filled*".equals(raw)) { + tooltip.append(Component.literal("• ").withStyle(ChatFormatting.DARK_GRAY)).append(Component.literal("*filled slot*").withStyle(ChatFormatting.GRAY)); + } else { + String display = party.displayMember(i); + tooltip.append(Component.literal("• ").withStyle(ChatFormatting.DARK_GRAY)).append(Component.literal(display).withStyle(i == 0 ? ChatFormatting.AQUA : ChatFormatting.WHITE)); + if (i == 0) { + tooltip.append(Component.literal(" (Host)").withStyle(ChatFormatting.YELLOW)); + } + } + } + int open = party.max() - party.size(); + if (open > 0) { + tooltip.append("\n").append(Component.literal("+ " + open + " open slot" + (open == 1 ? "" : "s")).withStyle(ChatFormatting.DARK_GRAY)); + } + return tooltip; + } } diff --git a/src/tel/eden/mod/gui/EdenMenuScreen.java b/src/tel/eden/mod/gui/EdenMenuScreen.java index 4da206e..304bba1 100644 --- a/src/tel/eden/mod/gui/EdenMenuScreen.java +++ b/src/tel/eden/mod/gui/EdenMenuScreen.java @@ -60,18 +60,11 @@ protected void init() { }).bounds(metrics.buttonX, metrics.startY + metrics.buttonPitch, metrics.buttonWidth, metrics.buttonHeight).build()); this.addRenderableWidget(Button.builder(Component.literal("Manage Party"), button -> { - String ign = EdenModClient.instance().playerName(); - tel.eden.mod.net.PartyInfo myParty = null; - for (tel.eden.mod.net.PartyInfo p : EdenModClient.instance().knownParties()) { - if (ign != null && p.host().equalsIgnoreCase(ign)) { - myParty = p; - break; - } - } + tel.eden.mod.net.PartyInfo myParty = EdenModClient.instance() != null ? EdenModClient.instance().activeParty() : null; if (myParty != null) { this.minecraft.setScreen(new PartyManageScreen(this, EdenModClient.instance(), myParty)); - } else { - this.minecraft.player.displayClientMessage(Component.literal("You are not hosting a party!").withStyle(net.minecraft.ChatFormatting.RED), false); + } else if (this.minecraft.player != null) { + this.minecraft.player.displayClientMessage(Component.literal("You are not in an active party!").withStyle(net.minecraft.ChatFormatting.RED), false); } }).bounds(metrics.buttonX, metrics.startY + (metrics.buttonPitch * 2), metrics.buttonWidth, metrics.buttonHeight).build()); diff --git a/src/tel/eden/mod/gui/PartyCreateScreen.java b/src/tel/eden/mod/gui/PartyCreateScreen.java index a224024..c20731e 100644 --- a/src/tel/eden/mod/gui/PartyCreateScreen.java +++ b/src/tel/eden/mod/gui/PartyCreateScreen.java @@ -1,7 +1,10 @@ package tel.eden.mod.gui; +import java.util.ArrayList; import java.util.LinkedHashSet; +import java.util.List; import java.util.Set; +import net.minecraft.ChatFormatting; import net.minecraft.client.Minecraft; import net.minecraft.client.gui.GuiGraphics; import net.minecraft.client.gui.components.Button; @@ -13,18 +16,20 @@ import net.minecraft.resources.Identifier; import tel.eden.mod.EdenModClient; -/** Screen GUI for creating a raid/Annihilation/Other party with auto-detected party size. */ +/** Screen GUI for creating a raid/Annihilation/Festival/Other party. */ public final class PartyCreateScreen extends EdenReferenceScreen { private static final int BASE_PANEL_WIDTH = 350; - private static final int BASE_PANEL_HEIGHT = 310; + private static final int BASE_PANEL_HEIGHT = 330; private final Screen parent; private final EdenModClient mod; private final String initialTarget; private final Set selectedTargets = new LinkedHashSet<>(); + private final List addedMembers = new ArrayList<>(); private int maxPartySize = 4; - private int playersInParty = 1; + private int filledSlots = 0; + private boolean sendToAlly = false; private Button btnNotg; private Button btnNol; @@ -33,22 +38,28 @@ public final class PartyCreateScreen extends EdenReferenceScreen { private Button btnWtp; private Button btnAnnihilation; private Button btnOther; + private Button btnFestival; private Button btnMaxMinus; private Button btnMaxPlus; - private Button btnPlayersMinus; - private Button btnPlayersPlus; + private Button btnFilledMinus; + private Button btnFilledPlus; + private Button btnAddPlayer; + private Button btnClearAdded; + private Button btnSendToAlly; private Button btnCreate; + private EditBox addPlayerField; private EditBox noteField; - private Identifier iconNotg; - private Identifier iconNol; - private Identifier iconTcc; - private Identifier iconTna; - private Identifier iconWtp; - private Identifier iconAnnihilation; - private Identifier iconOther; + private static final Identifier ICON_NOTG = Identifier.fromNamespaceAndPath("edenmod", "textures/gui/icons/notg.png"); + private static final Identifier ICON_NOL = Identifier.fromNamespaceAndPath("edenmod", "textures/gui/icons/nol.png"); + private static final Identifier ICON_TCC = Identifier.fromNamespaceAndPath("edenmod", "textures/gui/icons/tcc.png"); + private static final Identifier ICON_TNA = Identifier.fromNamespaceAndPath("edenmod", "textures/gui/icons/tna.png"); + private static final Identifier ICON_WTP = Identifier.fromNamespaceAndPath("edenmod", "textures/gui/icons/wtp.png"); + private static final Identifier ICON_ANNIHILATION = Identifier.fromNamespaceAndPath("edenmod", "textures/gui/icons/annihilation.png"); + private static final Identifier ICON_OTHER = Identifier.fromNamespaceAndPath("edenmod", "textures/gui/icons/other.png"); + private static final Identifier ICON_FESTIVAL = Identifier.fromNamespaceAndPath("edenmod", "textures/gui/icons/festival.png"); private EdenPanelLayout layout; public PartyCreateScreen(Screen parent, EdenModClient mod) { @@ -60,7 +71,7 @@ public PartyCreateScreen(Screen parent, EdenModClient mod, String initialTarget) this.parent = parent; this.mod = mod; this.initialTarget = initialTarget; - this.playersInParty = Math.max(1, Math.min(maxPartySize - 1, getScoreboardPartySize())); + this.filledSlots = 0; } @Override @@ -83,6 +94,7 @@ protected void init() { // Row 4 btnOther = Button.builder(Component.literal(" Other"), b -> onTargetClick("Other")).bounds(layout.x(15), layout.y(108), layout.w(155), layout.h(24)).build(); + btnFestival = Button.builder(Component.literal(" Festival"), b -> onTargetClick("Festival Minigames")).bounds(layout.x(180), layout.y(108), layout.w(155), layout.h(24)).build(); this.addRenderableWidget(btnNotg); this.addRenderableWidget(btnNol); @@ -91,54 +103,90 @@ protected void init() { this.addRenderableWidget(btnWtp); this.addRenderableWidget(btnAnnihilation); this.addRenderableWidget(btnOther); + this.addRenderableWidget(btnFestival); // Max Party Size Adjusters - btnMaxMinus = Button.builder(Component.literal("-"), b -> adjustMaxPartySize(-1)).bounds(layout.x(220), layout.y(142), layout.w(20), layout.h(20)).build(); - btnMaxPlus = Button.builder(Component.literal("+"), b -> adjustMaxPartySize(1)).bounds(layout.x(270), layout.y(142), layout.w(20), layout.h(20)).build(); - + btnMaxMinus = Button.builder(Component.literal("-"), b -> adjustMaxPartySize(-1)).bounds(layout.x(220), layout.y(136), layout.w(20), layout.h(18)).build(); + btnMaxPlus = Button.builder(Component.literal("+"), b -> adjustMaxPartySize(1)).bounds(layout.x(270), layout.y(136), layout.w(20), layout.h(18)).build(); this.addRenderableWidget(btnMaxMinus); this.addRenderableWidget(btnMaxPlus); - // Players in Party Adjusters - btnPlayersMinus = Button.builder(Component.literal("-"), b -> adjustPlayersInParty(-1)).bounds(layout.x(220), layout.y(164), layout.w(20), layout.h(20)).build(); - btnPlayersPlus = Button.builder(Component.literal("+"), b -> adjustPlayersInParty(1)).bounds(layout.x(270), layout.y(164), layout.w(20), layout.h(20)).build(); + // Filled Slots Adjusters + btnFilledMinus = Button.builder(Component.literal("-"), b -> adjustFilledSlots(-1)).bounds(layout.x(220), layout.y(158), layout.w(20), layout.h(18)).build(); + btnFilledPlus = Button.builder(Component.literal("+"), b -> adjustFilledSlots(1)).bounds(layout.x(270), layout.y(158), layout.w(20), layout.h(18)).build(); + this.addRenderableWidget(btnFilledMinus); + this.addRenderableWidget(btnFilledPlus); + + // Add Player Field + Button + addPlayerField = new EditBox(this.font, layout.x(15), layout.y(182), layout.w(125), layout.h(18), Component.literal("Player IGN")); + addPlayerField.setMaxLength(16); + this.addRenderableWidget(addPlayerField); + + btnAddPlayer = Button.builder(Component.literal("Add"), b -> onAddPlayer()).bounds(layout.x(145), layout.y(182), layout.w(45), layout.h(18)).build(); + this.addRenderableWidget(btnAddPlayer); + + // Send to Ally toggle + btnSendToAlly = Button.builder(Component.literal(sendToAlly ? "§a[✔] Ally" : "§7[ ] Ally"), b -> toggleSendToAlly()).bounds(layout.x(200), layout.y(182), layout.w(135), layout.h(18)).build(); + this.addRenderableWidget(btnSendToAlly); - this.addRenderableWidget(btnPlayersMinus); - this.addRenderableWidget(btnPlayersPlus); + // Clear Added Members Button (only visible when addedMembers is non-empty) + btnClearAdded = Button.builder(Component.literal("§cClear"), b -> onClearAdded()).bounds(layout.x(280), layout.y(204), layout.w(55), layout.h(16)).build(); + btnClearAdded.visible = !addedMembers.isEmpty(); + this.addRenderableWidget(btnClearAdded); // Note Text Field - noteField = new EditBox(this.font, layout.x(15), layout.y(206), layout.w(320), layout.h(20), Component.literal("Party Note")); + noteField = new EditBox(this.font, layout.x(15), layout.y(236), layout.w(320), layout.h(18), Component.literal("Party Note")); noteField.setMaxLength(100); this.addRenderableWidget(noteField); // Action Buttons - btnCreate = Button.builder(Component.literal("Create Party"), b -> onCreate()).bounds(layout.x(15), layout.y(276), layout.w(155), layout.h(20)).build(); - Button btnCancel = Button.builder(Component.literal("Cancel"), b -> onClose()).bounds(layout.x(180), layout.y(276), layout.w(155), layout.h(20)).build(); - + btnCreate = Button.builder(Component.literal("Create Party"), b -> onCreate()).bounds(layout.x(15), layout.y(294), layout.w(155), layout.h(20)).build(); + Button btnCancel = Button.builder(Component.literal("Cancel"), b -> onClose()).bounds(layout.x(180), layout.y(294), layout.w(155), layout.h(20)).build(); this.addRenderableWidget(btnCreate); this.addRenderableWidget(btnCancel); - // Register custom icon textures - iconNotg = registerDynamicIcon("notg"); - iconNol = registerDynamicIcon("nol"); - iconTcc = registerDynamicIcon("tcc"); - iconTna = registerDynamicIcon("tna"); - iconWtp = registerDynamicIcon("wtp"); - iconAnnihilation = registerDynamicIcon("annihilation"); - iconOther = registerDynamicIcon("other"); applyInitialTarget(); + trimToMax(); updateWidgetStates(); } + private int occupancy() { + return 1 + addedMembers.size() + filledSlots; + } + + private void trimToMax() { + while (occupancy() > maxPartySize) { + if (filledSlots > 0) { + filledSlots--; + } else if (!addedMembers.isEmpty()) { + addedMembers.remove(addedMembers.size() - 1); + } else { + break; + } + } + filledSlots = Math.min(filledSlots, maxAllowedFilledSlots()); + } + + private void toggleSendToAlly() { + sendToAlly = !sendToAlly; + btnSendToAlly.setMessage(Component.literal(sendToAlly ? "§a[✔] Ally" : "§7[ ] Ally")); + } + private void applyInitialTarget() { if (initialTarget == null || initialTarget.isBlank()) { return; } + if (initialTarget.equals("Festival Minigames") && (mod == null || !mod.isFestivalEnabled())) { + return; + } selectedTargets.clear(); if (initialTarget.equals("Annihilation")) { selectedTargets.add("Annihilation"); maxPartySize = 10; + } else if (initialTarget.equals("Festival Minigames")) { + selectedTargets.add("Festival Minigames"); + maxPartySize = 5; } else if (initialTarget.equals("Other")) { selectedTargets.add("Other"); maxPartySize = 4; @@ -146,30 +194,21 @@ private void applyInitialTarget() { selectedTargets.add(initialTarget); maxPartySize = 4; } - playersInParty = Math.max(1, Math.min(maxPartySize - 1, playersInParty)); } private void onTargetClick(String target) { - if (target.equals("Annihilation")) { - if (selectedTargets.contains("Annihilation")) { - selectedTargets.clear(); - maxPartySize = 4; - } else { - selectedTargets.clear(); - selectedTargets.add("Annihilation"); + if (target.equals("Annihilation") || target.equals("Festival Minigames") || target.equals("Other")) { + selectedTargets.clear(); + selectedTargets.add(target); + if (target.equals("Annihilation")) { maxPartySize = 10; - } - } else if (target.equals("Other")) { - if (selectedTargets.contains("Other")) { - selectedTargets.clear(); - maxPartySize = 4; + } else if (target.equals("Festival Minigames")) { + maxPartySize = 5; } else { - selectedTargets.clear(); - selectedTargets.add("Other"); maxPartySize = 4; } } else { - if (selectedTargets.contains("Annihilation") || selectedTargets.contains("Other")) { + if (selectedTargets.contains("Annihilation") || selectedTargets.contains("Festival Minigames") || selectedTargets.contains("Other")) { selectedTargets.clear(); } if (selectedTargets.contains(target)) { @@ -180,28 +219,63 @@ private void onTargetClick(String target) { maxPartySize = 4; } - if (playersInParty >= maxPartySize) { - playersInParty = maxPartySize - 1; + trimToMax(); + updateWidgetStates(); + } + + private void adjustMaxPartySize(int amount) { + if (selectedTargets.contains("Other") || selectedTargets.contains("Annihilation") || selectedTargets.contains("Festival Minigames")) { + maxPartySize = Math.max(Math.max(2, occupancy()), Math.min(10, maxPartySize + amount)); + updateWidgetStates(); } + } + + private int maxAllowedFilledSlots() { + return Math.max(0, maxPartySize - 2 - addedMembers.size()); + } + private void adjustFilledSlots(int amount) { + int maxAllowed = maxAllowedFilledSlots(); + filledSlots = Math.max(0, Math.min(maxAllowed, filledSlots + amount)); updateWidgetStates(); } - private void adjustMaxPartySize(int amount) { - if (selectedTargets.contains("Other") || selectedTargets.contains("Annihilation")) { - maxPartySize = Math.max(2, Math.min(10, maxPartySize + amount)); - if (playersInParty >= maxPartySize) { - playersInParty = maxPartySize - 1; + private void onAddPlayer() { + if (addPlayerField == null) + return; + String ign = addPlayerField.getValue().trim(); + if (ign.matches("[A-Za-z0-9_]{1,16}") && (mod == null || !ign.equalsIgnoreCase(mod.playerName())) && addedMembers.stream().noneMatch(m -> m.equalsIgnoreCase(ign))) { + if (occupancy() < maxPartySize) { + addedMembers.add(ign); + addPlayerField.setValue(""); + trimToMax(); + updateWidgetStates(); } - updateWidgetStates(); } } - private void adjustPlayersInParty(int amount) { - playersInParty = Math.max(1, Math.min(maxPartySize - 1, playersInParty + amount)); + private void onClearAdded() { + addedMembers.clear(); updateWidgetStates(); } + private String getCreateDisabledReason() { + if (selectedTargets.isEmpty()) { + return "Please select at least one target!"; + } + if (selectedTargets.contains("Annihilation") && (mod != null && !mod.isAnnihilationActive())) { + return "Annihilation parties can only be opened while the Annihilation timer is active. Check /party annihilation when."; + } + if (selectedTargets.contains("Festival Minigames") && (mod != null && !mod.isFestivalEnabled())) { + return "Festival Minigames party finder is currently disabled by Senate."; + } + int limit = maxAllowedFilledSlots(); + if (filledSlots > limit) { + return "Filled slots must be between 0 and " + limit + "."; + } + return null; + } + private void updateWidgetStates() { btnNotg.setMessage(Component.literal(getButtonText("Nest of the Grootslangs", "NOTG"))); btnNol.setMessage(Component.literal(getButtonText("Orphion's Nexus of Light", "NOL"))); @@ -210,15 +284,24 @@ private void updateWidgetStates() { btnWtp.setMessage(Component.literal(getButtonText("The Wartorn Palace", "WTP"))); btnAnnihilation.setMessage(Component.literal(getButtonText("Annihilation", "Annihilation"))); btnOther.setMessage(Component.literal(getButtonText("Other", "Other"))); + btnFestival.setMessage(Component.literal(getButtonText("Festival Minigames", "Festival"))); + btnFestival.active = mod != null && mod.isFestivalEnabled(); - boolean canEditMax = selectedTargets.contains("Other") || selectedTargets.contains("Annihilation"); - btnMaxMinus.active = canEditMax && maxPartySize > 2; + boolean canEditMax = selectedTargets.contains("Other") || selectedTargets.contains("Annihilation") || selectedTargets.contains("Festival Minigames"); + btnMaxMinus.active = canEditMax && maxPartySize > Math.max(2, occupancy()); btnMaxPlus.active = canEditMax && maxPartySize < 10; - btnPlayersMinus.active = playersInParty > 1; - btnPlayersPlus.active = playersInParty < (maxPartySize - 1); + btnFilledMinus.active = filledSlots > 0; + btnFilledPlus.active = filledSlots < maxAllowedFilledSlots(); - btnCreate.active = !selectedTargets.isEmpty(); + if (btnAddPlayer != null) { + btnAddPlayer.active = occupancy() < maxPartySize; + } + if (btnClearAdded != null) { + btnClearAdded.visible = !addedMembers.isEmpty(); + } + + btnCreate.active = (getCreateDisabledReason() == null); } private String getButtonText(String target, String displayName) { @@ -240,6 +323,8 @@ private String getShortTargetName(String target) { return "TNA"; if (target.equals("The Wartorn Palace")) return "WTP"; + if (target.equals("Festival Minigames")) + return "Festival"; return target; } @@ -255,45 +340,74 @@ public void render(GuiGraphics g, int mouseX, int mouseY, float delta) { super.render(g, scaledMouseX, scaledMouseY, delta); // Draw custom icons on buttons - drawButtonIcon(g, btnNotg, iconNotg); - drawButtonIcon(g, btnNol, iconNol); - drawButtonIcon(g, btnTcc, iconTcc); - drawButtonIcon(g, btnTna, iconTna); - drawButtonIcon(g, btnWtp, iconWtp); - drawButtonIcon(g, btnAnnihilation, iconAnnihilation); - drawButtonIcon(g, btnOther, iconOther); + drawButtonIcon(g, btnNotg, ICON_NOTG); + drawButtonIcon(g, btnNol, ICON_NOL); + drawButtonIcon(g, btnTcc, ICON_TCC); + drawButtonIcon(g, btnTna, ICON_TNA); + drawButtonIcon(g, btnWtp, ICON_WTP); + drawButtonIcon(g, btnAnnihilation, ICON_ANNIHILATION); + drawButtonIcon(g, btnOther, ICON_OTHER); + drawButtonIcon(g, btnFestival, ICON_FESTIVAL); // Draw Screen Title (Opaque White) g.drawCenteredString(this.font, this.title, layout.centerX(), layout.y(12), 0xFFFFFFFF); // Draw Labels & Values (Opaque Colors) - g.drawString(this.font, "Max Party Size:", layout.x(15), layout.y(148), 0xFFA0A0A0); - g.drawCenteredString(this.font, String.valueOf(this.maxPartySize), layout.x(255), layout.y(148), 0xFFFFFFFF); + g.drawString(this.font, "Max Party Size:", layout.x(15), layout.y(141), 0xFFA0A0A0); + g.drawCenteredString(this.font, String.valueOf(this.maxPartySize), layout.x(255), layout.y(141), 0xFFFFFFFF); + + g.drawString(this.font, "Filled Slots:", layout.x(15), layout.y(163), 0xFFA0A0A0); + g.drawCenteredString(this.font, String.valueOf(this.filledSlots), layout.x(255), layout.y(163), 0xFFFFFFFF); - g.drawString(this.font, "Players in Party:", layout.x(15), layout.y(170), 0xFFA0A0A0); - g.drawCenteredString(this.font, String.valueOf(this.playersInParty), layout.x(255), layout.y(170), 0xFFFFFFFF); + // Draw Added Members if present + if (!addedMembers.isEmpty()) { + String membersText = "§aMembers: §f" + String.join(", ", addedMembers); + if (this.font.width(membersText) > layout.w(255)) { + membersText = "§aMembers: §f" + addedMembers.size() + " added"; + } + g.drawString(this.font, membersText, layout.x(15), layout.y(208), 0xFFFFFFFF); + } - g.drawString(this.font, "Party Note:", layout.x(15), layout.y(194), 0xFFA0A0A0); + g.drawString(this.font, "Party Note:", layout.x(15), layout.y(224), 0xFFA0A0A0); // Draw Preview Message (Opaque Colors) if (selectedTargets.isEmpty()) { - g.drawCenteredString(this.font, "Please select at least one target!", layout.centerX(), layout.y(240), 0xFFFF5555); + g.drawCenteredString(this.font, "Please select at least one target!", layout.centerX(), layout.y(266), 0xFFFF5555); } else { String preview; + String scopeTag = sendToAlly ? "[Ally] " : "[Guild] "; if (selectedTargets.contains("Annihilation")) { - preview = "Annihilation (" + playersInParty + "/" + maxPartySize + ")"; + preview = scopeTag + "Annihilation (" + occupancy() + "/" + maxPartySize + ")"; + } else if (selectedTargets.contains("Festival Minigames")) { + preview = scopeTag + "Festival (" + occupancy() + "/" + maxPartySize + ")"; } else if (selectedTargets.contains("Other")) { - preview = "Other (" + playersInParty + "/" + maxPartySize + ")"; + preview = scopeTag + "Other (" + occupancy() + "/" + maxPartySize + ")"; } else { java.util.List shortNames = new java.util.ArrayList<>(); for (String target : selectedTargets) { shortNames.add(getShortTargetName(target)); } - preview = String.join(" / ", shortNames) + " (" + playersInParty + "/4)"; + preview = scopeTag + String.join(" / ", shortNames) + " (" + occupancy() + "/4)"; } - g.drawCenteredString(this.font, "Ready: " + preview, layout.centerX(), layout.y(240), 0xFF55FF55); + boolean ready = getCreateDisabledReason() == null; + g.drawCenteredString(this.font, (ready ? "Ready: " : "Preview: ") + preview, layout.centerX(), layout.y(266), ready ? 0xFF55FF55 : 0xFFA0A0A0); } popReferencePose(g); + + if (!btnCreate.active && isHovered(scaledMouseX, scaledMouseY, btnCreate)) { + String reason = getCreateDisabledReason(); + if (reason != null) { + g.setComponentTooltipForNextFrame(this.font, List.of(Component.literal(reason).withStyle(ChatFormatting.RED)), mouseX, mouseY); + } + } else if (mod != null && !mod.isAnnihilationActive() && isHovered(scaledMouseX, scaledMouseY, btnAnnihilation)) { + g.setComponentTooltipForNextFrame(this.font, List.of(Component.literal("Annihilation parties can only be opened while the Annihilation timer is active. Check /party annihilation when.").withStyle(ChatFormatting.RED)), mouseX, mouseY); + } else if (mod != null && !mod.isFestivalEnabled() && isHovered(scaledMouseX, scaledMouseY, btnFestival)) { + g.setComponentTooltipForNextFrame(this.font, List.of(Component.literal("Festival Minigames party finder is currently disabled by Senate.").withStyle(ChatFormatting.RED)), mouseX, mouseY); + } + } + + private static boolean isHovered(int mouseX, int mouseY, Button btn) { + return btn != null && mouseX >= btn.getX() && mouseX <= btn.getX() + btn.getWidth() && mouseY >= btn.getY() && mouseY <= btn.getY() + btn.getHeight(); } private void drawButtonIcon(GuiGraphics g, Button btn, Identifier icon) { @@ -315,36 +429,22 @@ private void onCreate() { String targetName; if (selectedTargets.contains("Annihilation")) { targetName = "Annihilation"; + } else if (selectedTargets.contains("Festival Minigames")) { + targetName = "Festival Minigames"; } else if (selectedTargets.contains("Other")) { targetName = "Other"; } else { targetName = String.join(" / ", selectedTargets); } - int filledSlots = playersInParty - 1; var ws = mod.socket(); if (ws != null) { - ws.sendPartyOpen(targetName, maxPartySize, noteField.getValue(), filledSlots); + ws.sendPartyOpen(targetName, maxPartySize, noteField.getValue(), filledSlots, sendToAlly, mod.currentWynnWorld(), addedMembers); } onClose(); } - private int getScoreboardPartySize() { - try { - var player = Minecraft.getInstance().player; - if (player != null) { - var scoreboard = player.level().getScoreboard(); - var team = scoreboard.getPlayersTeam(player.getScoreboardName()); - if (team != null && team.getName().startsWith("p_")) { - return Math.max(1, team.getPlayers().size()); - } - } - } catch (Exception ignored) { - } - return 1; - } - @Override public void onClose() { this.minecraft.setScreen(this.parent); @@ -369,21 +469,4 @@ public boolean mouseDragged(MouseButtonEvent event, double d, double e) { public boolean mouseScrolled(double mouseX, double mouseY, double d, double e) { return super.mouseScrolled(mouseX / uiScale, mouseY / uiScale, d, e); } - private Identifier registerDynamicIcon(String name) { - Identifier loc = Identifier.parse("edenmod:dynamic_icon/" + name); - var tm = Minecraft.getInstance().getTextureManager(); - try { - var stream = PartyCreateScreen.class.getClassLoader().getResourceAsStream("assets/edenmod/textures/gui/icons/" + name + ".png"); - if (stream != null) { - var img = com.mojang.blaze3d.platform.NativeImage.read(stream); - var texture = new net.minecraft.client.renderer.texture.DynamicTexture(() -> name, img); - tm.register(loc, texture); - stream.close(); - return loc; - } - } catch (Exception e) { - e.printStackTrace(); - } - return null; - } } diff --git a/src/tel/eden/mod/gui/PartyListScreen.java b/src/tel/eden/mod/gui/PartyListScreen.java index 512ce5b..34a20f7 100644 --- a/src/tel/eden/mod/gui/PartyListScreen.java +++ b/src/tel/eden/mod/gui/PartyListScreen.java @@ -13,13 +13,14 @@ public final class PartyListScreen extends EdenReferenceScreen { private static final int BASE_PANEL_WIDTH = 420; private static final int BASE_PANEL_HEIGHT = 300; - private static final int ROW_HEIGHT = 28; - private static final int VISIBLE_ROWS = 8; + private static final int ROW_HEIGHT = 44; + private static final int VISIBLE_ROWS = 5; private final Screen parent; private final EdenModClient mod; private final List lastKnownParties = new ArrayList<>(); + private List rawKnownParties = List.of(); private EdenPanelLayout layout; private int scrollOffset; @@ -51,8 +52,9 @@ private void requestPartyList() { } private void refreshPartySnapshot() { + rawKnownParties = mod != null ? mod.knownParties() : List.of(); lastKnownParties.clear(); - lastKnownParties.addAll(mod.knownParties()); + lastKnownParties.addAll(rawKnownParties); lastKnownParties.sort(java.util.Comparator.comparing(PartyListScreen::isFull)); scrollOffset = Math.max(0, Math.min(scrollOffset, Math.max(0, lastKnownParties.size() - VISIBLE_ROWS))); } @@ -64,7 +66,7 @@ private static boolean isFull(PartyInfo party) { @Override public void tick() { super.tick(); - if (!lastKnownParties.equals(mod.knownParties())) { + if (mod != null && !rawKnownParties.equals(mod.knownParties())) { refreshPartySnapshot(); } } @@ -88,6 +90,8 @@ public void render(GuiGraphics g, int mouseX, int mouseY, float delta) { int listHeight = layout.h(ROW_HEIGHT * VISIBLE_ROWS); g.fill(listLeft, listTop, listLeft + listWidth, listTop + listHeight, 0x22000000); + PartyInfo hoveredParty = null; + if (lastKnownParties.isEmpty()) { g.drawCenteredString(this.font, "No active parties", layout.centerX(), layout.y(122), 0xFFAAAAAA); } else { @@ -97,32 +101,141 @@ public void render(GuiGraphics g, int mouseX, int mouseY, float delta) { break; } - int rowTop = layout.y(38 + visible * ROW_HEIGHT); - int rowBottom = rowTop + layout.h(24); - int joinLeft = layout.x(333); - int joinRight = layout.x(389); + PartyInfo party = lastKnownParties.get(index); + int cardTop = layout.y(38 + visible * ROW_HEIGHT); + int cardBottom = cardTop + layout.h(40); + int cardLeft = layout.x(17); + int cardRight = layout.x(391); + + boolean cardHovered = scaledMouseX >= cardLeft && scaledMouseX <= cardRight && scaledMouseY >= cardTop && scaledMouseY <= cardBottom; + if (cardHovered) { + hoveredParty = party; + } - g.fill(layout.x(17), rowTop, layout.x(391), rowBottom, 0x44282828); - drawJoinButton(g, joinLeft, rowTop, joinRight - joinLeft, layout.h(24), isJoinHovered(scaledMouseX, scaledMouseY, index)); + // Card background and subtle borders + g.fill(cardLeft, cardTop, cardRight, cardBottom, cardHovered ? 0x55353535 : 0x44252525); + g.fill(cardLeft, cardTop, cardRight, cardTop + 1, cardHovered ? 0x44FFFFFF : 0x22FFFFFF); + g.fill(cardLeft, cardBottom - 1, cardRight, cardBottom, 0x22000000); + + // Right side: Join / Full / In Party button + int btnLeft = layout.x(335); + int btnRight = layout.x(387); + int btnWidth = btnRight - btnLeft; + int btnTop = cardTop + layout.h(9); + int btnHeight = layout.h(22); + + boolean isSelf = mod != null && party.members().stream().anyMatch(m -> m.equalsIgnoreCase(mod.playerName())); + boolean isFull = isFull(party); + boolean joinHovered = cardHovered && scaledMouseX >= btnLeft && scaledMouseX <= btnRight && scaledMouseY >= btnTop && scaledMouseY <= btnTop + btnHeight; + + String btnLabel; + int btnTextColor; + boolean btnActive; + if (isSelf) { + btnLabel = "In Party"; + btnTextColor = 0xFFAAAAAA; + btnActive = false; + } else if (isFull) { + btnLabel = "Full"; + btnTextColor = 0xFFFF7777; + btnActive = false; + } else { + btnLabel = "Join"; + btnTextColor = 0xFFFFFFFF; + btnActive = true; + } + drawJoinButton(g, btnLeft, btnTop, btnWidth, btnHeight, btnLabel, btnTextColor, btnActive, joinHovered && btnActive); + + // Occupancy count badge before the button + int badgeRight = layout.x(330); + String countStr = party.size() + "/" + party.max(); + int countWidth = this.font.width(countStr); + g.drawString(this.font, countStr, badgeRight - countWidth, cardTop + layout.h(16), isFull ? 0xFFFF6666 : 0xFF55FF55); + + // Text lines on left + int textLeft = layout.x(22); + int textWidth = badgeRight - countWidth - textLeft - layout.w(8); + + // Line 1: Scope, ID, World, Raid name + String scope = party.ally() ? "§b[Ally] " : "§6[Guild] "; + String worldStr = party.world() != null && !party.world().isBlank() ? "§a[" + party.world() + "] " : ""; + String line1 = scope + "§f#" + party.id() + " " + worldStr + "§f§l" + party.raid(); + g.drawString(this.font, trimToWidth(line1, textWidth), textLeft, cardTop + layout.h(5), 0xFFFFFFFF); + + // Line 2: Host & Members + String hostStr = "§eHost: §f" + party.displayMember(0); + String line2; + if (party.size() > 1) { + List others = new ArrayList<>(); + for (int i = 1; i < party.size(); i++) { + others.add(party.displayMember(i)); + } + line2 = hostStr + " §7| §bMembers: §f" + String.join(", ", others); + } else { + line2 = hostStr + " §7| §8(Waiting for members)"; + } + g.drawString(this.font, trimToWidth(line2, textWidth), textLeft, cardTop + layout.h(17), 0xFFFFFFFF); - PartyInfo party = lastKnownParties.get(index); - String text = "#" + party.id() + " | " + party.raid() + " | Host: " + party.host() + " | [" + party.members().size() + "/" + party.max() + "]"; - g.drawString(this.font, trimToWidth(text, layout.w(306)), layout.x(24), rowTop + layout.h(8), 0xFFFFFFFF); - g.drawCenteredString(this.font, "Join", joinLeft + (layout.w(56) / 2), rowTop + layout.h(8), 0xFFFFFFFF); + // Line 3: Note (if present) + if (party.note() != null && !party.note().isBlank()) { + String line3 = "§8Note: §7" + party.note(); + g.drawString(this.font, trimToWidth(line3, textWidth), textLeft, cardTop + layout.h(28), 0xFFFFFFFF); + } } } layout.drawScrollbar(g, layout.x(393), listTop, layout.w(8), listHeight, VISIBLE_ROWS, lastKnownParties.size(), scrollOffset); popReferencePose(g); + + if (hoveredParty != null) { + g.setComponentTooltipForNextFrame(this.font, buildPartyTooltip(hoveredParty), mouseX, mouseY); + } + } + + private List buildPartyTooltip(PartyInfo party) { + List lines = new ArrayList<>(); + String scope = party.ally() ? "§b[Ally Party]" : "§6[Guild Party]"; + lines.add(Component.literal(scope + " §f#" + party.id() + " - §a§l" + party.raid())); + if (party.world() != null && !party.world().isBlank()) { + lines.add(Component.literal("§7World: §e" + party.world())); + } + lines.add(Component.literal("§eHost: §f" + party.displayMember(0))); + if (party.size() > 1) { + lines.add(Component.literal("§bMembers (" + (party.size() - 1) + "):")); + for (int i = 1; i < party.size(); i++) { + lines.add(Component.literal(" §7- §f" + party.displayMember(i))); + } + } + int openSlots = party.max() - party.size(); + if (openSlots > 0) { + lines.add(Component.literal("§8 + " + openSlots + " open slot" + (openSlots == 1 ? "" : "s"))); + } + if (party.note() != null && !party.note().isBlank()) { + lines.add(Component.literal("§8Note: §f" + party.note())); + } + boolean isSelf = mod != null && party.members().stream().anyMatch(m -> m.equalsIgnoreCase(mod.playerName())); + if (isSelf) { + lines.add(Component.literal("§7(You are already in this party)")); + } else if (isFull(party)) { + lines.add(Component.literal("§c(Party is full)")); + } else { + lines.add(Component.literal("§aClick \"Join\" to join this party")); + } + return lines; } - private void drawJoinButton(GuiGraphics g, int x, int y, int width, int height, boolean hovered) { - int fill = hovered ? 0xFF777777 : 0xFF5A5A5A; + private void drawJoinButton(GuiGraphics g, int x, int y, int width, int height, String label, int textColor, boolean active, boolean hovered) { + int fill = !active ? 0xFF353535 : (hovered ? 0xFF2E7D32 : 0xFF1E5224); + int borderTop = !active ? 0xFF4A4A4A : (hovered ? 0xFF4CAF50 : 0xFF2E7D32); + int borderBottom = !active ? 0xFF222222 : (hovered ? 0xFF1B5E20 : 0xFF143B17); + g.fill(x, y, x + width, y + height, fill); - g.fill(x, y, x + width, y + 1, 0xFFD8D8D8); - g.fill(x, y, x + 1, y + height, 0xFFD8D8D8); - g.fill(x + width - 1, y, x + width, y + height, 0xFF222222); - g.fill(x, y + height - 1, x + width, y + height, 0xFF222222); + g.fill(x, y, x + width, y + 1, borderTop); + g.fill(x, y, x + 1, y + height, borderTop); + g.fill(x + width - 1, y, x + width, y + height, borderBottom); + g.fill(x, y + height - 1, x + width, y + height, borderBottom); + + g.drawCenteredString(this.font, label, x + (width / 2), y + ((height - 8) / 2), textColor); } @Override @@ -203,7 +316,7 @@ private int rowAt(double mouseX, double mouseY) { } int row = ((int) mouseY - layout.y(36)) / layout.h(ROW_HEIGHT); int index = scrollOffset + row; - return index >= 0 && index < lastKnownParties.size() ? index : -1; + return index >= 0 && index < lastKnownParties.size() && row < VISIBLE_ROWS ? index : -1; } private boolean isJoinHovered(double mouseX, double mouseY, int index) { @@ -211,8 +324,14 @@ private boolean isJoinHovered(double mouseX, double mouseY, int index) { if (visibleRow < 0 || visibleRow >= VISIBLE_ROWS) { return false; } - int rowTop = layout.y(38 + visibleRow * ROW_HEIGHT); - return mouseX >= layout.x(333) && mouseX <= layout.x(389) && mouseY >= rowTop && mouseY <= rowTop + layout.h(24); + PartyInfo party = lastKnownParties.get(index); + boolean isSelf = mod != null && party.members().stream().anyMatch(m -> m.equalsIgnoreCase(mod.playerName())); + if (isSelf || isFull(party)) { + return false; + } + int cardTop = layout.y(38 + visibleRow * ROW_HEIGHT); + int btnTop = cardTop + layout.h(9); + return mouseX >= layout.x(335) && mouseX <= layout.x(387) && mouseY >= btnTop && mouseY <= btnTop + layout.h(22); } private void updateScrollFromMouse(double mouseY) { diff --git a/src/tel/eden/mod/gui/PartyManageScreen.java b/src/tel/eden/mod/gui/PartyManageScreen.java index c06b3cc..21e80f6 100644 --- a/src/tel/eden/mod/gui/PartyManageScreen.java +++ b/src/tel/eden/mod/gui/PartyManageScreen.java @@ -25,20 +25,25 @@ public final class PartyManageScreen extends EdenReferenceScreen { private final Screen parent; private final EdenModClient mod; - private final PartyInfo party; + private PartyInfo party; private int filledSlots; private int maxSize; private EditBox noteField; + private EditBox addPlayerField; private Button btnPlayersMinus; private Button btnPlayersPlus; private Button btnMaxMinus; private Button btnMaxPlus; + private Button btnOpenToAllies; + private Button btnAddPlayer; private Identifier iconTarget; - private final Map headCache = new HashMap<>(); - private final Set headFetching = new HashSet<>(); + private static final Map HEAD_CACHE = new java.util.concurrent.ConcurrentHashMap<>(); + private static final Set HEAD_FETCHING = java.util.concurrent.ConcurrentHashMap.newKeySet(); + private static final Map HEAD_FAILURES = new java.util.concurrent.ConcurrentHashMap<>(); + private static final long HEAD_RETRY_DELAY_MS = 60_000L; private EdenPanelLayout layout; public PartyManageScreen(Screen parent, EdenModClient mod, PartyInfo party) { @@ -46,15 +51,69 @@ public PartyManageScreen(Screen parent, EdenModClient mod, PartyInfo party) { this.parent = parent; this.mod = mod; this.party = party; + this.filledSlots = filledCount(party); + this.maxSize = party.max(); + } - int fSlots = 0; + private static int filledCount(PartyInfo party) { + int filled = 0; for (String member : party.members()) { if (member.equals("*filled*")) { - fSlots++; + filled++; + } + } + return filled; + } + + @Override + public void tick() { + super.tick(); + if (mod != null) { + boolean found = false; + for (PartyInfo p : mod.knownParties()) { + if (p.id() == party.id()) { + found = true; + if (!p.equals(party)) { + // Keep the host's unsent size edits; take the server's values for the rest. + boolean maxEdited = this.maxSize != party.max(); + boolean filledEdited = this.filledSlots != filledCount(party); + this.party = p; + if (!maxEdited) { + this.maxSize = p.max(); + } + if (!filledEdited) { + this.filledSlots = filledCount(p); + } + + String curNote = noteField != null ? noteField.getValue() : null; + boolean noteFocused = noteField != null && noteField.isFocused(); + String curAdd = addPlayerField != null ? addPlayerField.getValue() : null; + boolean addFocused = addPlayerField != null && addPlayerField.isFocused(); + + rebuildWidgets(); + + if (noteField != null) { + if (noteFocused && curNote != null) { + noteField.setValue(curNote); + noteField.setFocused(true); + } else { + noteField.setValue(p.note()); + } + } + if (addPlayerField != null && curAdd != null) { + addPlayerField.setValue(curAdd); + if (addFocused) { + addPlayerField.setFocused(true); + } + } + } + return; + } + } + if (!found && mod.socket() != null && mod.socket().isConnected()) { + onClose(); } } - this.filledSlots = fSlots; - this.maxSize = party.max(); } @Override @@ -63,65 +122,138 @@ protected void init() { layout = EdenPanelLayout.centered(virtualWidth, virtualHeight, BASE_PANEL_WIDTH, BASE_PANEL_HEIGHT); int rightColX = layout.x(190); - btnMaxMinus = Button.builder(Component.literal("-"), b -> adjustMax(-1)).bounds(layout.x(245), layout.y(100), layout.w(20), layout.h(20)).build(); - btnMaxPlus = Button.builder(Component.literal("+"), b -> adjustMax(1)).bounds(layout.x(295), layout.y(100), layout.w(20), layout.h(20)).build(); + String self = mod != null ? mod.playerName() : null; + boolean isHost = self != null && party.host().equalsIgnoreCase(self); + + // Max size adjusters + btnMaxMinus = Button.builder(Component.literal("-"), b -> adjustMax(-1)).bounds(layout.x(245), layout.y(106), layout.w(20), layout.h(18)).build(); + btnMaxPlus = Button.builder(Component.literal("+"), b -> adjustMax(1)).bounds(layout.x(295), layout.y(106), layout.w(20), layout.h(18)).build(); + btnMaxMinus.active = isHost && party.isCustomSize(); + btnMaxPlus.active = isHost && party.isCustomSize(); this.addRenderableWidget(btnMaxMinus); this.addRenderableWidget(btnMaxPlus); - btnPlayersMinus = Button.builder(Component.literal("-"), b -> adjustFilledSlots(-1)).bounds(layout.x(245), layout.y(124), layout.w(20), layout.h(20)).build(); - btnPlayersPlus = Button.builder(Component.literal("+"), b -> adjustFilledSlots(1)).bounds(layout.x(295), layout.y(124), layout.w(20), layout.h(20)).build(); + // Filled slots adjusters + btnPlayersMinus = Button.builder(Component.literal("-"), b -> adjustFilledSlots(-1)).bounds(layout.x(245), layout.y(128), layout.w(20), layout.h(18)).build(); + btnPlayersPlus = Button.builder(Component.literal("+"), b -> adjustFilledSlots(1)).bounds(layout.x(295), layout.y(128), layout.w(20), layout.h(18)).build(); + btnPlayersMinus.active = isHost; + btnPlayersPlus.active = isHost; this.addRenderableWidget(btnPlayersMinus); this.addRenderableWidget(btnPlayersPlus); - noteField = new EditBox(this.font, rightColX, layout.y(175), layout.w(160), layout.h(20), Component.literal("Party Note")); + // Open to Allies toggle button + btnOpenToAllies = Button.builder(Component.literal(party.ally() ? "§a[✔] Open to Allies" : "§7[ ] Open to Allies"), b -> onToggleAlly()).bounds(rightColX, layout.y(152), layout.w(160), layout.h(18)).build(); + btnOpenToAllies.active = isHost; + this.addRenderableWidget(btnOpenToAllies); + + // Note field + noteField = new EditBox(this.font, rightColX, layout.y(186), layout.w(160), layout.h(18), Component.literal("Party Note")); noteField.setMaxLength(100); noteField.setValue(party.note()); + noteField.setEditable(isHost); this.addRenderableWidget(noteField); - Button btnCreateInGame = Button.builder(Component.literal("Create In-game"), b -> { - mod.createInGameParty(party.members()); - onClose(); - }).bounds(rightColX, layout.y(220), layout.w(160), layout.h(20)).build(); - Button btnUpdate = Button.builder(Component.literal("Update Party"), b -> onUpdate()).bounds(rightColX, layout.y(245), layout.w(160), layout.h(20)).build(); - Button btnDisband = Button.builder(Component.literal("§cDisband Party"), b -> onDisband()).bounds(rightColX, layout.y(270), layout.w(160), layout.h(20)).build(); - Button btnCancel = Button.builder(Component.literal("Cancel"), b -> onClose()).bounds(rightColX, layout.y(295), layout.w(160), layout.h(20)).build(); - - this.addRenderableWidget(btnCreateInGame); - this.addRenderableWidget(btnUpdate); - this.addRenderableWidget(btnDisband); - this.addRenderableWidget(btnCancel); + if (isHost) { + Button btnCreateInGame = Button.builder(Component.literal("Create In-game"), b -> { + mod.createInGameParty(party.members()); + onClose(); + }).bounds(rightColX, layout.y(214), layout.w(160), layout.h(18)).build(); + Button btnUpdate = Button.builder(Component.literal("Update Party"), b -> onUpdate()).bounds(rightColX, layout.y(236), layout.w(160), layout.h(18)).build(); + Button btnDisband = Button.builder(Component.literal("§cDisband Party"), b -> onDisband()).bounds(rightColX, layout.y(258), layout.w(160), layout.h(18)).build(); + Button btnCancel = Button.builder(Component.literal("Close"), b -> onClose()).bounds(rightColX, layout.y(280), layout.w(160), layout.h(18)).build(); + + this.addRenderableWidget(btnCreateInGame); + this.addRenderableWidget(btnUpdate); + this.addRenderableWidget(btnDisband); + this.addRenderableWidget(btnCancel); + } else { + Button btnLeave = Button.builder(Component.literal("§cLeave Party"), b -> onLeave()).bounds(rightColX, layout.y(258), layout.w(160), layout.h(18)).build(); + Button btnCancel = Button.builder(Component.literal("Close"), b -> onClose()).bounds(rightColX, layout.y(280), layout.w(160), layout.h(18)).build(); + + this.addRenderableWidget(btnLeave); + this.addRenderableWidget(btnCancel); + } + // Target icon String target = party.raid().toLowerCase(); if (target.contains("grootslangs")) - iconTarget = registerDynamicIcon("notg"); + iconTarget = Identifier.fromNamespaceAndPath("edenmod", "textures/gui/icons/notg.png"); else if (target.contains("nexus")) - iconTarget = registerDynamicIcon("nol"); + iconTarget = Identifier.fromNamespaceAndPath("edenmod", "textures/gui/icons/nol.png"); else if (target.contains("colossus")) - iconTarget = registerDynamicIcon("tcc"); + iconTarget = Identifier.fromNamespaceAndPath("edenmod", "textures/gui/icons/tcc.png"); else if (target.contains("anomaly")) - iconTarget = registerDynamicIcon("tna"); + iconTarget = Identifier.fromNamespaceAndPath("edenmod", "textures/gui/icons/tna.png"); else if (target.contains("palace")) - iconTarget = registerDynamicIcon("wtp"); + iconTarget = Identifier.fromNamespaceAndPath("edenmod", "textures/gui/icons/wtp.png"); else if (target.contains("annihilation")) - iconTarget = registerDynamicIcon("annihilation"); + iconTarget = Identifier.fromNamespaceAndPath("edenmod", "textures/gui/icons/annihilation.png"); + else if (target.contains("festival")) + iconTarget = Identifier.fromNamespaceAndPath("edenmod", "textures/gui/icons/festival.png"); else - iconTarget = registerDynamicIcon("other"); + iconTarget = Identifier.fromNamespaceAndPath("edenmod", "textures/gui/icons/other.png"); - int yPos = layout.y(40); + // Left column: Member rows (Kick button) + int yPos = layout.y(38); + int memberIdx = 0; for (String member : party.members()) { - if (member.equals("*filled*")) + if (member.equals("*filled*")) { + memberIdx++; continue; - if (!member.equalsIgnoreCase(party.host())) { - String finalMember = member; - Button btnKick = Button.builder(Component.literal("§c\u2716"), b -> onKick(finalMember)).bounds(layout.x(145), yPos - Math.max(1, layout.h(3)), layout.w(20), layout.h(20)).build(); + } + final String currentMember = member; + + // Kick button (host only, cannot kick self) + if (isHost && !member.equalsIgnoreCase(party.host())) { + Button btnKick = Button.builder(Component.literal("§c\u2716"), b -> onKick(currentMember)).bounds(layout.x(148), yPos - Math.max(1, layout.h(2)), layout.w(18), layout.h(16)).build(); this.addRenderableWidget(btnKick); } + yPos += layout.h(24); + memberIdx++; + } + + // Add Player by IGN (Host only, bottom of left column) + if (isHost) { + addPlayerField = new EditBox(this.font, layout.x(10), layout.y(280), layout.w(110), layout.h(18), Component.literal("IGN")); + addPlayerField.setMaxLength(16); + this.addRenderableWidget(addPlayerField); + + btnAddPlayer = Button.builder(Component.literal("Add"), b -> onAddPlayer()).bounds(layout.x(124), layout.y(280), layout.w(44), layout.h(18)).build(); + this.addRenderableWidget(btnAddPlayer); } updateWidgetStates(); } + private void onToggleAlly() { + var ws = mod != null ? mod.socket() : null; + if (ws != null) { + ws.sendPartyAllyToggle(!party.ally()); + } + } + + private void onAddPlayer() { + if (addPlayerField == null) + return; + String ign = addPlayerField.getValue().trim(); + if (ign.matches("[A-Za-z0-9_]{1,16}") && (realMembers() + filledSlots < maxSize) && party.members().stream().noneMatch(m -> m.equalsIgnoreCase(ign))) { + var ws = mod != null ? mod.socket() : null; + if (ws != null) { + ws.sendPartyManage("add", null, 0, ign); + addPlayerField.setValue(""); + } + } + } + + private void onLeave() { + var ws = mod != null ? mod.socket() : null; + if (ws != null) { + ws.sendPartyLeave(party.id()); + } + onClose(); + } + /** Members occupying a real slot (i.e. excluding the ``*filled*`` placeholders). */ private int realMembers() { int filled = 0; @@ -146,12 +278,22 @@ private void adjustMax(int amount) { } private void updateWidgetStates() { + String self = mod != null ? mod.playerName() : null; + boolean isHost = self != null && party.host().equalsIgnoreCase(self); + int real = realMembers(); int occupancy = real + filledSlots; - btnPlayersMinus.active = filledSlots > 0; - btnPlayersPlus.active = occupancy < maxSize; - btnMaxMinus.active = maxSize > Math.max(2, occupancy); - btnMaxPlus.active = maxSize < 10; + btnPlayersMinus.active = isHost && filledSlots > 0; + btnPlayersPlus.active = isHost && occupancy < maxSize; + btnMaxMinus.active = isHost && party.isCustomSize() && maxSize > Math.max(2, occupancy); + btnMaxPlus.active = isHost && party.isCustomSize() && maxSize < 10; + if (btnOpenToAllies != null) { + btnOpenToAllies.active = isHost; + btnOpenToAllies.setMessage(Component.literal(party.ally() ? "§a[✔] Open to Allies" : "§7[ ] Open to Allies")); + } + if (btnAddPlayer != null) { + btnAddPlayer.active = isHost && occupancy < maxSize; + } } @Override @@ -167,19 +309,24 @@ public void render(GuiGraphics g, int mouseX, int mouseY, float delta) { super.render(g, scaledMouseX, scaledMouseY, delta); g.drawCenteredString(this.font, this.title, layout.centerX(), layout.y(12), 0xFFFFFFFF); - g.drawCenteredString(this.font, "Party Members (" + (realMembers() + filledSlots) + "/" + maxSize + ")", layout.x(87), layout.y(20), 0xFF55FF55); + String scopeTag = party.ally() ? "§b[Ally] " : "§6[Guild] "; + g.drawCenteredString(this.font, scopeTag + "Party (" + (realMembers() + filledSlots) + "/" + maxSize + ")", layout.x(87), layout.y(20), 0xFF55FF55); - int yPos = layout.y(40); + int yPos = layout.y(38); int headSize = Math.max(8, layout.h(12)); + int memberIdx = 0; for (String member : party.members()) { - if (member.equals("*filled*")) + if (member.equals("*filled*")) { + memberIdx++; continue; + } - Identifier headId = headCache.get(member); + Identifier headId = HEAD_CACHE.get(member); if (headId == null) { g.fill(layout.x(10), yPos - 1, layout.x(10) + headSize, yPos - 1 + headSize, 0xFF444444); - if (!headFetching.contains(member)) { - headFetching.add(member); + Long lastFail = HEAD_FAILURES.get(member); + boolean canRetry = lastFail == null || (System.currentTimeMillis() - lastFail > HEAD_RETRY_DELAY_MS); + if (canRetry && HEAD_FETCHING.add(member)) { java.util.concurrent.CompletableFuture.runAsync(() -> fetchHead(member)); } } else { @@ -190,62 +337,69 @@ public void render(GuiGraphics g, int mouseX, int mouseY, float delta) { g.pose().popMatrix(); } - g.drawString(this.font, member, layout.x(28), yPos + 1, member.equalsIgnoreCase(party.host()) ? 0xFFFFD700 : 0xFFFFFFFF); + String displayName = party.displayMember(memberIdx); + g.drawString(this.font, displayName, layout.x(26), yPos + 2, member.equalsIgnoreCase(party.host()) ? 0xFFFFD700 : 0xFFFFFFFF); yPos += layout.h(24); + memberIdx++; } int extraRows = Math.max(0, maxSize - realMembers()); for (int i = 0; i < extraRows; i++) { + if (yPos > layout.y(270)) + break; g.fill(layout.x(10), yPos - 1, layout.x(10) + headSize, yPos - 1 + headSize, 0xFF222222); - g.drawString(this.font, i < filledSlots ? "§7*Filled Slot*" : "§8*Empty*", layout.x(28), yPos + 1, 0xFFAAAAAA); + g.drawString(this.font, i < filledSlots ? "§7*Filled Slot*" : "§8*Empty*", layout.x(26), yPos + 2, 0xFFAAAAAA); yPos += layout.h(24); } if (iconTarget != null) { - int iconSize = Math.max(16, layout.h(28)); + int iconSize = Math.max(16, layout.h(24)); g.pose().pushMatrix(); - g.pose().translate(layout.x(256), layout.y(40)); + g.pose().translate(layout.x(258), layout.y(34)); g.pose().scale(iconSize / 64.0f, iconSize / 64.0f); g.blit(RenderPipelines.GUI_TEXTURED, iconTarget, 0, 0, 0.0f, 0.0f, 64, 64, 64, 64); g.pose().popMatrix(); } - g.drawCenteredString(this.font, party.raid(), layout.x(270), layout.y(74), 0xFF55FF55); + g.drawCenteredString(this.font, party.raid(), layout.x(270), layout.y(62), 0xFF55FF55); + if (party.world() != null && !party.world().isBlank()) { + g.drawCenteredString(this.font, "§eServer: " + party.world(), layout.x(270), layout.y(74), 0xFFFFFF55); + } - g.drawString(this.font, "Max Size:", layout.x(190), layout.y(106), 0xFFAAAAAA); - g.drawCenteredString(this.font, String.valueOf(this.maxSize), layout.x(280), layout.y(106), 0xFFFFFFFF); + g.drawString(this.font, "Max Size:", layout.x(190), layout.y(110), 0xFFAAAAAA); + g.drawCenteredString(this.font, String.valueOf(this.maxSize), layout.x(275), layout.y(110), 0xFFFFFFFF); - g.drawString(this.font, "Filled Slots:", layout.x(190), layout.y(130), 0xFFAAAAAA); - g.drawCenteredString(this.font, String.valueOf(this.filledSlots), layout.x(280), layout.y(130), 0xFFFFFFFF); + g.drawString(this.font, "Filled Slots:", layout.x(190), layout.y(132), 0xFFAAAAAA); + g.drawCenteredString(this.font, String.valueOf(this.filledSlots), layout.x(275), layout.y(132), 0xFFFFFFFF); - g.drawString(this.font, "Party Note:", layout.x(190), layout.y(162), 0xFFAAAAAA); + g.drawString(this.font, "Party Note:", layout.x(190), layout.y(174), 0xFFAAAAAA); popReferencePose(g); } private void onUpdate() { - var ws = mod.socket(); + var ws = mod != null ? mod.socket() : null; if (ws != null) { - if (this.maxSize != party.max()) { + // Size edits go out once here, not per click, so +/- doesn't re-render the Discord embed each time. + // A shrinking party drops its filled slots before its max, or the bot rejects the max as below occupancy. + boolean maxChanged = this.maxSize != party.max(); + boolean shrinking = this.maxSize < party.max(); + if (maxChanged && !shrinking) { ws.sendPartyManage("max", null, this.maxSize, null); } - - if (!noteField.getValue().equals(party.note())) { - ws.sendPartyManage("note", noteField.getValue(), 0, null); + if (this.filledSlots != filledCount(party)) { + ws.sendPartyManage("filled", null, this.filledSlots, null); } - - int fSlots = 0; - for (String member : party.members()) { - if (member.equals("*filled*")) - fSlots++; + if (maxChanged && shrinking) { + ws.sendPartyManage("max", null, this.maxSize, null); } - if (this.filledSlots != fSlots) { - ws.sendPartyManage("filled", null, this.filledSlots, null); + if (noteField != null && !noteField.getValue().equals(party.note())) { + ws.sendPartyManage("note", noteField.getValue(), 0, null); } } onClose(); } private void onDisband() { - var ws = mod.socket(); + var ws = mod != null ? mod.socket() : null; if (ws != null) { ws.sendPartyManage("close", null, 0, null); } @@ -253,11 +407,10 @@ private void onDisband() { } private void onKick(String ign) { - var ws = mod.socket(); + var ws = mod != null ? mod.socket() : null; if (ws != null) { ws.sendPartyManage("remove", null, 0, ign); } - onClose(); } @Override @@ -284,42 +437,38 @@ public boolean mouseDragged(MouseButtonEvent event, double d, double e) { public boolean mouseScrolled(double mouseX, double mouseY, double d, double e) { return super.mouseScrolled(mouseX / uiScale, mouseY / uiScale, d, e); } - private Identifier registerDynamicIcon(String name) { - Identifier loc = Identifier.parse("edenmod:dynamic_icon/" + name); - var tm = Minecraft.getInstance().getTextureManager(); - try { - var stream = PartyManageScreen.class.getClassLoader().getResourceAsStream("assets/edenmod/textures/gui/icons/" + name + ".png"); - if (stream != null) { - var img = NativeImage.read(stream); - var texture = new DynamicTexture(() -> name, img); - tm.register(loc, texture); - stream.close(); - return loc; - } - } catch (Exception e) { - e.printStackTrace(); - } - return null; - } private void fetchHead(String member) { if (!member.matches("[A-Za-z0-9_]{1,16}")) return; + NativeImage img = null; try { - java.net.URLConnection conn = TlsPolicy.open(new java.net.URL("https://minotar.net/helm/" + member + "/12.png")); + java.net.URLConnection conn = TlsPolicy.open(java.net.URI.create("https://minotar.net/helm/" + member + "/12.png").toURL()); conn.setConnectTimeout(5_000); conn.setReadTimeout(5_000); try (java.io.InputStream stream = conn.getInputStream()) { - var img = NativeImage.read(stream); - Minecraft.getInstance().execute(() -> { + img = NativeImage.read(stream); + } + final NativeImage finalImg = img; + Minecraft.getInstance().execute(() -> { + try { Identifier loc = Identifier.parse("edenmod:head/" + member.toLowerCase()); - var texture = new DynamicTexture(() -> member, img); + var texture = new DynamicTexture(() -> member, finalImg); Minecraft.getInstance().getTextureManager().register(loc, texture); - headCache.put(member, loc); - }); - } + HEAD_CACHE.put(member, loc); + HEAD_FAILURES.remove(member); + } catch (Exception e) { + finalImg.close(); + } finally { + HEAD_FETCHING.remove(member); + } + }); } catch (Exception e) { - // Silently fail if head cannot be fetched + if (img != null) { + img.close(); + } + HEAD_FAILURES.put(member, System.currentTimeMillis()); + HEAD_FETCHING.remove(member); } } } diff --git a/src/tel/eden/mod/net/BridgeWebSocketClient.java b/src/tel/eden/mod/net/BridgeWebSocketClient.java index ba7408f..150f8f3 100644 --- a/src/tel/eden/mod/net/BridgeWebSocketClient.java +++ b/src/tel/eden/mod/net/BridgeWebSocketClient.java @@ -116,6 +116,10 @@ default void onRewardOptOutReply(String rewardKind, boolean blocked, String erro /** A short result line for a party action the player just took in-game. */ void onPartyFeedback(String message, String color); + /** Party configuration update (e.g. Festival Minigames toggle, Annihilation active). */ + default void onPartyConfig(boolean festivalEnabled, boolean annihilationActive) { + } + /** Feedback on game commands (e.g. cooldown). */ void onGameFeedback(String message, String color); @@ -549,8 +553,8 @@ private boolean sendTypeIfConnected(String type) { return true; } - /** Open a new party in-game for the given label (raid name or Annihilation). */ - public void sendPartyOpen(String raid, int maxSize, String note, int filled) { + /** Open a new party in-game for the given label (raid name, Annihilation, or Festival). */ + public void sendPartyOpen(String raid, int maxSize, String note, int filled, boolean ally, String world, java.util.List members) { WebSocket current = socket; if (current == null) { return; @@ -565,9 +569,30 @@ public void sendPartyOpen(String raid, int maxSize, String note, int filled) { if (filled > 0) { obj.addProperty("filled", filled); } + if (ally) { + obj.addProperty("ally", true); + } + if (world != null && !world.isBlank()) { + obj.addProperty("world", world); + } + if (members != null && !members.isEmpty()) { + com.google.gson.JsonArray arr = new com.google.gson.JsonArray(); + for (String m : members) { + arr.add(m); + } + obj.add("members", arr); + } enqueueSend(current, obj.toString()); } + public void sendPartyOpen(String raid, int maxSize, String note, int filled, boolean ally, String world) { + sendPartyOpen(raid, maxSize, note, filled, ally, world, java.util.List.of()); + } + + public void sendPartyOpen(String raid, int maxSize, String note, int filled) { + sendPartyOpen(raid, maxSize, note, filled, false, ""); + } + /** Join the open raid party with the given id. */ public void sendPartyJoin(int id) { WebSocket current = socket; @@ -580,6 +605,10 @@ public void sendPartyJoin(int id) { enqueueSend(current, obj.toString()); } + public void sendPartyAllyToggle(boolean ally) { + sendPartyManage("ally", ally ? "true" : "false", ally ? 1 : 0, ""); + } + /** Leave a raid party ({@code null} id = whichever party you are in). */ public void sendPartyLeave(Integer id) { WebSocket current = socket; @@ -1395,7 +1424,17 @@ private void handlePayload(String payload) { case "rewardOptOutReply" -> sink.onRewardOptOutReply(get(obj, "rewardKind"), getBool(obj, "blocked"), get(obj, "error"), get(obj, "color")); case "giftLockReply" -> sink.onGiftLockReply(getBool(obj, "granted"), get(obj, "error"), get(obj, "holder"), getInt(obj, "retryAfterSeconds", 0)); case "partyUpdate" -> sink.onPartyUpdate(get(obj, "event"), get(obj, "actor"), parseParty(obj), get(obj, "color")); - case "partyListReply" -> sink.onPartyList(parsePartyList(obj), get(obj, "color")); + case "partyListReply" -> { + if (sink != null && obj.has("festivalEnabled")) { + sink.onPartyConfig(getBool(obj, "festivalEnabled"), getBool(obj, "annihilationActive")); + } + sink.onPartyList(parsePartyList(obj), get(obj, "color")); + } + case "partyConfig" -> { + if (sink != null) { + sink.onPartyConfig(getBool(obj, "festivalEnabled"), getBool(obj, "annihilationActive")); + } + } case "partyFeedback" -> sink.onPartyFeedback(get(obj, "message"), get(obj, "color")); case "gameFeedback" -> sink.onGameFeedback(get(obj, "message"), get(obj, "color")); case "pillMessage" -> sink.onPillMessage(get(obj, "label"), get(obj, "content"), get(obj, "color"), getBool(obj, "bold")); @@ -1556,7 +1595,7 @@ private static float getFloat(JsonObject obj, String key, float fallback) { } private static PartyInfo parseParty(JsonObject obj) { - return new PartyInfo(getInt(obj, "id", 0), get(obj, "raid"), get(obj, "host"), getStringArray(obj, "members"), getInt(obj, "max", 4), get(obj, "note")); + return new PartyInfo(getInt(obj, "id", 0), get(obj, "raid"), get(obj, "host"), getStringArray(obj, "members"), getInt(obj, "max", 4), get(obj, "note"), getStringArray(obj, "tags"), getBool(obj, "ally"), get(obj, "world")); } private static java.util.List parsePartyList(JsonObject obj) { diff --git a/src/tel/eden/mod/net/PartyInfo.java b/src/tel/eden/mod/net/PartyInfo.java index 088e4d3..d787624 100644 --- a/src/tel/eden/mod/net/PartyInfo.java +++ b/src/tel/eden/mod/net/PartyInfo.java @@ -6,13 +6,43 @@ * A raid party snapshot received from the bridge backend (in a {@code partyUpdate} * event or a {@code partyListReply}). {@code members} is host-first. */ -public record PartyInfo(int id, String raid, String host, List members, int max, String note) { +public record PartyInfo(int id, String raid, String host, List members, int max, String note, List tags, boolean ally, String world) { public PartyInfo { members = List.copyOf(members); + tags = tags != null ? List.copyOf(tags) : List.of(); + world = world != null ? world : ""; + } + + public PartyInfo(int id, String raid, String host, List members, int max, String note) { + this(id, raid, host, members, max, note, List.of(), false, ""); } /** Current occupancy (e.g. {@code 2} of {@code max}). */ public int size() { return members.size(); } + + public String tagOf(int index) { + if (index >= 0 && index < tags.size()) { + String t = tags.get(index); + if (t != null) + return t; + } + return ""; + } + + public String displayMember(int index) { + if (index < 0 || index >= members.size()) + return ""; + String m = members.get(index); + String tag = tagOf(index); + if (!tag.isEmpty()) { + return "[" + tag + "] " + m; + } + return m; + } + + public boolean isCustomSize() { + return "Annihilation".equalsIgnoreCase(raid) || "Festival Minigames".equalsIgnoreCase(raid) || "Other".equalsIgnoreCase(raid); + } }