From 9e2e6ec00a2db4688bcfd7dba02b12c9b630b70a Mon Sep 17 00:00:00 2001 From: IWOSS Date: Sun, 10 May 2026 19:36:45 +0300 Subject: [PATCH 1/2] fix(ui): harden surveyor clicks and realm management --- .../PoliticalEntityColorPaletteScreen.java | 179 ++++++++++++++++++ .../gui/war/PoliticalEntityListScreen.java | 9 +- .../civilian/SettlementSurveyorToolItem.java | 24 ++- .../catalog/CivilianPacketCatalog.java | 1 + .../civilian/MessageUseSurveyorBlock.java | 64 +++++++ .../registry/PoliticalRegistryValidation.java | 3 + .../assets/bannermod/lang/en_us.json | 6 + .../assets/bannermod/lang/ru_ru.json | 6 + ...calEntityColorPaletteVerificationTest.java | 32 ++++ ...eyorInteractionPacketVerificationTest.java | 30 +++ ...PoliticalRegistryCreateValidationTest.java | 34 ++++ 11 files changed, 372 insertions(+), 16 deletions(-) create mode 100644 src/main/java/com/talhanation/bannermod/client/military/gui/war/PoliticalEntityColorPaletteScreen.java create mode 100644 src/main/java/com/talhanation/bannermod/network/messages/civilian/MessageUseSurveyorBlock.java create mode 100644 src/test/java/com/talhanation/bannermod/client/military/gui/PoliticalEntityColorPaletteVerificationTest.java create mode 100644 src/test/java/com/talhanation/bannermod/settlement/SurveyorInteractionPacketVerificationTest.java create mode 100644 src/test/java/com/talhanation/bannermod/war/registry/PoliticalRegistryCreateValidationTest.java diff --git a/src/main/java/com/talhanation/bannermod/client/military/gui/war/PoliticalEntityColorPaletteScreen.java b/src/main/java/com/talhanation/bannermod/client/military/gui/war/PoliticalEntityColorPaletteScreen.java new file mode 100644 index 00000000..c5fe994e --- /dev/null +++ b/src/main/java/com/talhanation/bannermod/client/military/gui/war/PoliticalEntityColorPaletteScreen.java @@ -0,0 +1,179 @@ +package com.talhanation.bannermod.client.military.gui.war; + +import com.talhanation.bannermod.client.military.gui.MilitaryGuiStyle; +import com.talhanation.bannermod.war.registry.PoliticalColorParser; +import net.minecraft.client.Minecraft; +import net.minecraft.client.gui.Font; +import net.minecraft.client.gui.GuiGraphics; +import net.minecraft.client.gui.components.Button; +import net.minecraft.client.gui.components.Tooltip; +import net.minecraft.client.gui.screens.Screen; +import net.minecraft.network.chat.Component; + +import java.util.function.Consumer; + +public class PoliticalEntityColorPaletteScreen extends Screen { + private static final int PANEL_W = 252; + private static final int PANEL_H = 194; + private static final String[] PALETTE = { + "F9FFFE", "F9801D", "C74EBD", "3AB3DA", + "FED83D", "80C71F", "F38BAA", "474F52", + "9D9D97", "169C9C", "8932B8", "3C44AA", + "835432", "5E7C16", "B02E26", "1D1D21" + }; + + private final Screen parent; + private final Consumer onSubmit; + private String currentColor; + private int guiLeft; + private int guiTop; + + public PoliticalEntityColorPaletteScreen(Screen parent, String currentColor, Consumer onSubmit) { + super(Component.translatable("gui.bannermod.states.dialog.color.title")); + this.parent = parent; + this.currentColor = currentColor == null ? "" : currentColor.trim(); + this.onSubmit = onSubmit; + } + + @Override + protected void init() { + super.init(); + this.guiLeft = (this.width - PANEL_W) / 2; + this.guiTop = (this.height - PANEL_H) / 2; + + int swatchSize = 28; + int gap = 6; + int gridLeft = this.guiLeft + 22; + int gridTop = this.guiTop + 72; + for (int i = 0; i < PALETTE.length; i++) { + String hex = PALETTE[i]; + int column = i % 4; + int row = i / 4; + addRenderableWidget(new SwatchButton( + gridLeft + column * (swatchSize + gap), + gridTop + row * (swatchSize + gap), + swatchSize, + hex, + button -> choose(hex))); + } + + addRenderableWidget(Button.builder(Component.translatable("gui.bannermod.states.palette.clear"), button -> choose("")) + .bounds(this.guiLeft + 150, this.guiTop + 82, 80, 20) + .tooltip(Tooltip.create(Component.translatable("gui.bannermod.states.palette.clear.tooltip"))) + .build()); + addRenderableWidget(Button.builder(Component.translatable("gui.bannermod.states.palette.custom"), button -> openCustomDialog()) + .bounds(this.guiLeft + 150, this.guiTop + 108, 80, 20) + .tooltip(Tooltip.create(Component.translatable("gui.bannermod.states.palette.custom.tooltip"))) + .build()); + addRenderableWidget(Button.builder(Component.translatable("gui.bannermod.common.back"), button -> onClose()) + .bounds(this.guiLeft + 150, this.guiTop + 134, 80, 20) + .build()); + } + + private void choose(String color) { + this.currentColor = color == null ? "" : color; + this.onSubmit.accept(this.currentColor); + this.minecraft.setScreen(this.parent); + } + + private void applyCustomColor(String color) { + this.currentColor = color == null ? "" : color; + this.onSubmit.accept(this.currentColor); + } + + private void openCustomDialog() { + Minecraft.getInstance().setScreen(new PoliticalEntityNameInputScreen( + this, + Component.translatable("gui.bannermod.states.dialog.color.title"), + Component.translatable("gui.bannermod.states.dialog.color.prompt"), + this.currentColor, + this::applyCustomColor, + 9, + true)); + } + + @Override + public void render(GuiGraphics graphics, int mouseX, int mouseY, float partialTick) { + graphics.fill(0, 0, this.width, this.height, 0x66000000); + MilitaryGuiStyle.parchmentPanel(graphics, this.guiLeft, this.guiTop, PANEL_W, PANEL_H); + MilitaryGuiStyle.titleStrip(graphics, this.guiLeft + 6, this.guiTop + 6, PANEL_W - 12, 14); + MilitaryGuiStyle.drawCenteredTitle(graphics, this.font, this.title, this.guiLeft, this.guiTop + 9, PANEL_W); + + graphics.drawString(this.font, + Component.translatable("gui.bannermod.states.palette.current", currentLabel()), + this.guiLeft + 18, + this.guiTop + 28, + MilitaryGuiStyle.TEXT_DARK, + false); + graphics.drawString(this.font, + Component.translatable("gui.bannermod.states.palette.hint"), + this.guiLeft + 18, + this.guiTop + 44, + MilitaryGuiStyle.TEXT_MUTED, + false); + + int previewColor = PoliticalColorParser.parseArgb(this.currentColor, 0xFFB8A17A); + graphics.fill(this.guiLeft + 18, this.guiTop + 58, this.guiLeft + 230, this.guiTop + 60, 0x665A4025); + graphics.fill(this.guiLeft + 150, this.guiTop + 30, this.guiLeft + 230, this.guiTop + 72, 0xFF201810); + graphics.fill(this.guiLeft + 154, this.guiTop + 34, this.guiLeft + 226, this.guiTop + 68, previewColor); + graphics.renderOutline(this.guiLeft + 150, this.guiTop + 30, 80, 42, 0xFF8A6A3A); + + super.render(graphics, mouseX, mouseY, partialTick); + } + + private Component currentLabel() { + if (this.currentColor.isBlank()) { + return Component.translatable("gui.bannermod.common.none"); + } + return Component.literal(this.currentColor); + } + + private static boolean sameColor(String left, String right) { + return PoliticalColorParser.parseArgb(left, Integer.MIN_VALUE) + == PoliticalColorParser.parseArgb(right, Integer.MAX_VALUE); + } + + @Override + public void onClose() { + if (this.parent != null) { + this.minecraft.setScreen(this.parent); + } else { + super.onClose(); + } + } + + @Override + public boolean isPauseScreen() { + return false; + } + + private final class SwatchButton extends Button { + private final String hexColor; + private final int argbColor; + + private SwatchButton(int x, int y, int size, String hexColor, OnPress onPress) { + super(x, y, size, size, Component.empty(), onPress, DEFAULT_NARRATION); + this.hexColor = hexColor; + this.argbColor = PoliticalColorParser.parseArgb(hexColor); + setTooltip(Tooltip.create(Component.literal("#" + hexColor))); + } + + @Override + protected void renderWidget(GuiGraphics graphics, int mouseX, int mouseY, float partialTick) { + int x = getX(); + int y = getY(); + int size = getWidth(); + boolean hovered = isHoveredOrFocused(); + boolean selected = sameColor(currentColor, this.hexColor); + int border = selected ? 0xFFE0B86A : hovered ? 0xFFB8A17A : 0xFF5A4025; + + graphics.fill(x, y, x + size, y + size, 0xFF201810); + graphics.fill(x + 2, y + 2, x + size - 2, y + size - 2, this.argbColor); + graphics.renderOutline(x, y, size, size, border); + if (selected) { + Font font = Minecraft.getInstance().font; + graphics.drawCenteredString(font, "*", x + size / 2, y + (size - 8) / 2, 0xFFFFFFFF); + } + } + } +} diff --git a/src/main/java/com/talhanation/bannermod/client/military/gui/war/PoliticalEntityListScreen.java b/src/main/java/com/talhanation/bannermod/client/military/gui/war/PoliticalEntityListScreen.java index f4b7acf5..e78e7dec 100644 --- a/src/main/java/com/talhanation/bannermod/client/military/gui/war/PoliticalEntityListScreen.java +++ b/src/main/java/com/talhanation/bannermod/client/military/gui/war/PoliticalEntityListScreen.java @@ -260,15 +260,10 @@ private void promoteToState() { private void openColorDialog() { if (this.selected == null) return; UUID id = this.selected.id(); - // 9 chars: optional '#' + up to 8 hex digits (covers AARRGGBB). - Minecraft.getInstance().setScreen(new PoliticalEntityNameInputScreen( + Minecraft.getInstance().setScreen(new PoliticalEntityColorPaletteScreen( this, - text("gui.bannermod.states.dialog.color.title"), - text("gui.bannermod.states.dialog.color.prompt"), this.selected.color(), - value -> sendColor(id, value), - 9, - /* allowEmpty */ true + value -> sendColor(id, value) )); } diff --git a/src/main/java/com/talhanation/bannermod/items/civilian/SettlementSurveyorToolItem.java b/src/main/java/com/talhanation/bannermod/items/civilian/SettlementSurveyorToolItem.java index 37d04dcd..b9017b80 100644 --- a/src/main/java/com/talhanation/bannermod/items/civilian/SettlementSurveyorToolItem.java +++ b/src/main/java/com/talhanation/bannermod/items/civilian/SettlementSurveyorToolItem.java @@ -1,6 +1,8 @@ package com.talhanation.bannermod.items.civilian; +import com.talhanation.bannermod.bootstrap.BannerModMain; import com.talhanation.bannermod.client.civilian.gui.SettlementSurveyorScreen; +import com.talhanation.bannermod.network.messages.civilian.MessageUseSurveyorBlock; import com.talhanation.bannermod.settlement.building.ZoneRole; import com.talhanation.bannermod.settlement.building.ZoneSelection; import com.talhanation.bannermod.settlement.validation.SurveyorDraftSuggestionService; @@ -65,21 +67,32 @@ public InteractionResult useOn(UseOnContext context) { } if (level.isClientSide) { + // Keep surveyor mode changes and block clicks on the same packet stream so a + // fresh farm click cannot race an older fort session on the server. + BannerModMain.SIMPLE_CHANNEL.sendToServer(new MessageUseSurveyorBlock(context.getHand(), clicked)); return InteractionResult.SUCCESS; } + return InteractionResult.SUCCESS; + } + + public static void handleBlockClick(ServerPlayer player, ItemStack stack, BlockPos clicked) { + if (player == null || stack == null || clicked == null) { + return; + } + ValidationSession session = getOrCreateSession(player, stack); if (session.anchorPos().equals(BlockPos.ZERO)) { SurveyorSessionCodec.write(stack, session.withAnchor(clicked)); player.sendSystemMessage(Component.translatable("bannermod.surveyor.anchor_set", clicked.toShortString()).withStyle(ChatFormatting.AQUA)); - return InteractionResult.SUCCESS; + return; } CompoundTag tag = ItemStackComponentData.read(stack); if (!tag.contains(TAG_PENDING_CORNER)) { ItemStackComponentData.update(stack, data -> data.putLong(TAG_PENDING_CORNER, clicked.asLong())); player.sendSystemMessage(Component.translatable("bannermod.surveyor.corner_a", clicked.toShortString()).withStyle(ChatFormatting.AQUA)); - return InteractionResult.SUCCESS; + return; } BlockPos cornerA = BlockPos.of(tag.getLong(TAG_PENDING_CORNER)); @@ -90,7 +103,6 @@ public InteractionResult useOn(UseOnContext context) { SurveyorSessionCodec.write(stack, updated); player.sendSystemMessage(Component.translatable("bannermod.surveyor.zone_captured", roleLabel(role)).withStyle(ChatFormatting.GREEN)); maybeAdvanceRoleAfterCapture(player, stack, updated, role); - return InteractionResult.SUCCESS; } @Override @@ -265,12 +277,6 @@ public static boolean hasAnyMarks(ItemStack stack) { || session != null && (!session.anchorPos().equals(BlockPos.ZERO) || !session.selections().isEmpty()); } - private static SurveyorMode nextMode(SurveyorMode mode) { - SurveyorMode[] modes = SurveyorMode.values(); - int idx = mode.ordinal(); - return modes[(idx + 1) % modes.length]; - } - public static void setSelectedRole(ItemStack stack, ZoneRole role) { if (stack == null || role == null) { return; diff --git a/src/main/java/com/talhanation/bannermod/network/catalog/CivilianPacketCatalog.java b/src/main/java/com/talhanation/bannermod/network/catalog/CivilianPacketCatalog.java index a203b36d..75f1b7f2 100644 --- a/src/main/java/com/talhanation/bannermod/network/catalog/CivilianPacketCatalog.java +++ b/src/main/java/com/talhanation/bannermod/network/catalog/CivilianPacketCatalog.java @@ -32,6 +32,7 @@ public final class CivilianPacketCatalog { MessageRequestRegisterBuilding.class, MessageSetSurveyorMode.class, MessageSetSurveyorRole.class, + MessageUseSurveyorBlock.class, MessageModifySurveyorSession.class, MessageValidateSurveyorSession.class, MessageToClientOpenWorkerScreen.class, diff --git a/src/main/java/com/talhanation/bannermod/network/messages/civilian/MessageUseSurveyorBlock.java b/src/main/java/com/talhanation/bannermod/network/messages/civilian/MessageUseSurveyorBlock.java new file mode 100644 index 00000000..b3a80e6a --- /dev/null +++ b/src/main/java/com/talhanation/bannermod/network/messages/civilian/MessageUseSurveyorBlock.java @@ -0,0 +1,64 @@ +package com.talhanation.bannermod.network.messages.civilian; + +import com.talhanation.bannermod.items.civilian.SettlementSurveyorToolItem; +import com.talhanation.bannermod.network.compat.BannerModNetworkContext; +import com.talhanation.bannermod.network.payload.BannerModMessage; +import net.minecraft.core.BlockPos; +import net.minecraft.network.FriendlyByteBuf; +import net.minecraft.network.protocol.PacketFlow; +import net.minecraft.server.level.ServerPlayer; +import net.minecraft.world.InteractionHand; +import net.minecraft.world.item.ItemStack; +import net.minecraft.world.phys.Vec3; + +public class MessageUseSurveyorBlock implements BannerModMessage { + private static final double MAX_CLICK_DISTANCE_SQR = 49.0D; + + public int handIndex; + public BlockPos clickedPos; + + public MessageUseSurveyorBlock() { + this.clickedPos = BlockPos.ZERO; + } + + public MessageUseSurveyorBlock(InteractionHand hand, BlockPos clickedPos) { + this.handIndex = hand == InteractionHand.OFF_HAND ? 1 : 0; + this.clickedPos = clickedPos == null ? BlockPos.ZERO : clickedPos.immutable(); + } + + @Override + public PacketFlow getExecutingSide() { + return BannerModMessage.serverbound(); + } + + @Override + public void executeServerSide(BannerModNetworkContext context) { + context.enqueueWork(() -> { + ServerPlayer player = context.getSender(); + if (player == null) { + return; + } + if (player.getEyePosition().distanceToSqr(Vec3.atCenterOf(this.clickedPos)) > MAX_CLICK_DISTANCE_SQR) { + return; + } + ItemStack stack = player.getItemInHand(handIndex == 1 ? InteractionHand.OFF_HAND : InteractionHand.MAIN_HAND); + if (!(stack.getItem() instanceof SettlementSurveyorToolItem)) { + return; + } + SettlementSurveyorToolItem.handleBlockClick(player, stack, this.clickedPos); + }); + } + + @Override + public MessageUseSurveyorBlock fromBytes(FriendlyByteBuf buf) { + this.handIndex = buf.readVarInt(); + this.clickedPos = buf.readBlockPos(); + return this; + } + + @Override + public void toBytes(FriendlyByteBuf buf) { + buf.writeVarInt(this.handIndex); + buf.writeBlockPos(this.clickedPos == null ? BlockPos.ZERO : this.clickedPos); + } +} diff --git a/src/main/java/com/talhanation/bannermod/war/registry/PoliticalRegistryValidation.java b/src/main/java/com/talhanation/bannermod/war/registry/PoliticalRegistryValidation.java index 93d9979a..97500af5 100644 --- a/src/main/java/com/talhanation/bannermod/war/registry/PoliticalRegistryValidation.java +++ b/src/main/java/com/talhanation/bannermod/war/registry/PoliticalRegistryValidation.java @@ -24,6 +24,9 @@ public static Result validateCreate(String name, UUID leaderUuid, Collection MAX_CLICK_DISTANCE_SQR")); + assertTrue(useBlockMessage.contains("SettlementSurveyorToolItem.handleBlockClick(player, stack, this.clickedPos);")); + assertTrue(packetCatalog.contains("MessageUseSurveyorBlock.class")); + } + + private static String read(String relativePath) throws IOException { + return Files.readString(ROOT.resolve(relativePath)); + } +} diff --git a/src/test/java/com/talhanation/bannermod/war/registry/PoliticalRegistryCreateValidationTest.java b/src/test/java/com/talhanation/bannermod/war/registry/PoliticalRegistryCreateValidationTest.java new file mode 100644 index 00000000..e25b82df --- /dev/null +++ b/src/test/java/com/talhanation/bannermod/war/registry/PoliticalRegistryCreateValidationTest.java @@ -0,0 +1,34 @@ +package com.talhanation.bannermod.war.registry; + +import net.minecraft.core.BlockPos; +import org.junit.jupiter.api.Test; + +import java.util.UUID; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; + +class PoliticalRegistryCreateValidationTest { + private static final UUID LEADER = UUID.fromString("00000000-0000-0000-0000-0000000000aa"); + + @Test + void validateCreateRejectsSecondEntityForSameLeader() { + PoliticalRegistryRuntime runtime = new PoliticalRegistryRuntime(); + runtime.create("Acadia", LEADER, BlockPos.ZERO, "", "", "", "", 0L).orElseThrow(); + + PoliticalRegistryValidation.Result validation = runtime.canCreate("Brittany", LEADER); + + assertFalse(validation.valid()); + assertEquals("leader_already_has_entity", validation.reason()); + } + + @Test + void createAllowsDifferentLeaders() { + PoliticalRegistryRuntime runtime = new PoliticalRegistryRuntime(); + runtime.create("Acadia", LEADER, BlockPos.ZERO, "", "", "", "", 0L).orElseThrow(); + + assertTrue(runtime.create("Brittany", UUID.fromString("00000000-0000-0000-0000-0000000000bb"), BlockPos.ZERO, + "", "", "", "", 0L).isPresent()); + } +} From 0b55511fdfdefc9e577e5305754bf0cff5386262 Mon Sep 17 00:00:00 2001 From: IWOSS Date: Sun, 10 May 2026 23:54:11 +0300 Subject: [PATCH 2/2] update(ui): add charter editor and co-leader picker --- docs/BANNERMOD_ALMANAC.html | 4 +- .../gui/war/PoliticalEntityCharterScreen.java | 131 ++++++++++ .../PoliticalEntityCoLeaderPickerScreen.java | 226 ++++++++++++++++++ .../gui/war/PoliticalEntityListScreen.java | 36 ++- .../assets/bannermod/lang/en_us.json | 15 +- .../assets/bannermod/lang/ru_ru.json | 15 +- ...yCharterAndCoLeaderUiVerificationTest.java | 41 ++++ 7 files changed, 453 insertions(+), 15 deletions(-) create mode 100644 src/main/java/com/talhanation/bannermod/client/military/gui/war/PoliticalEntityCharterScreen.java create mode 100644 src/main/java/com/talhanation/bannermod/client/military/gui/war/PoliticalEntityCoLeaderPickerScreen.java create mode 100644 src/test/java/com/talhanation/bannermod/client/military/gui/PoliticalEntityCharterAndCoLeaderUiVerificationTest.java diff --git a/docs/BANNERMOD_ALMANAC.html b/docs/BANNERMOD_ALMANAC.html index a11e1c92..8f2ceaca 100644 --- a/docs/BANNERMOD_ALMANAC.html +++ b/docs/BANNERMOD_ALMANAC.html @@ -130,7 +130,7 @@

Stances and combat rules

9. States And Government

-

A state is the political actor behind settlements, claims, wars, and allies. Create one with /bannermod state create <name>, inspect with list and info, set capital with setcapital, and change status with status. War Room state screens now show a visible ledger line for waiting-sync, select-first, read-only authority, and next server-checked steps. If you only want another player to share claim and settlement access, use the claim editor's Trusted Members list instead of making them a co-leader.

+

A state is the political actor behind settlements, claims, wars, and allies. Create one with /bannermod state create <name>, inspect with list and info, set capital with setcapital, and change status with status. War Room state screens now show a visible ledger line for waiting-sync, select-first, read-only authority, and next server-checked steps. Charter opens a dedicated editor that explains where the text appears, and Add co-leader now opens an online player picker while keeping manual nickname or UUID entry for offline targets. If you only want another player to share claim and settlement access, use the claim editor's Trusted Members list instead of making them a co-leader.

Government forms

Monarchy keeps core political authority leader-only. Republic lets co-leaders share authority for status, capital, colors, charter, claim edits, ally invites, siege placement, and legal war outcomes. Operators can perform explicitly admin-only outcomes. Trusted Members are different: they may build and manage local work areas in the claim, but they do not get claim-edit or war/state authority.

Promotion

@@ -273,7 +273,7 @@

Стойки и правила боя

9. Государство и правление

-

Государство — политическое лицо поселений, участков, войн и союзов. Создание: /bannermod state create <название>. Просмотр: list и info. Столица: setcapital. Статус: status. Если нужно просто поделиться доступом к клейму и поселению, используй список Доверенные игроки в редакторе клейма, а не со-лидера.

+

Государство — политическое лицо поселений, участков, войн и союзов. Создание: /bannermod state create <название>. Просмотр: list и info. Столица: setcapital. Статус: status. Кнопка Хартия теперь открывает отдельный редактор и сразу объясняет, где будет виден текст, а Добавить со-лидера открывает список онлайн-игроков, но сохраняет ручной ввод ника или UUID для оффлайн-целей. Если нужно просто поделиться доступом к клейму и поселению, используй список Доверенные игроки в редакторе клейма, а не со-лидера.

Формы правления

Монархия оставляет основные решения лидеру. Республика даёт соправителям общие полномочия: статус, столица, цвета, устав, правки участков, приглашение союзников, постановка осады и законные итоги войны. Операторы сервера могут выполнять действия, явно отмеченные как административные. Доверенные игроки работают иначе: они могут строить и управлять местными рабочими зонами в клейме, но не получают власти над государством, дипломатией или войной.

Повышение

diff --git a/src/main/java/com/talhanation/bannermod/client/military/gui/war/PoliticalEntityCharterScreen.java b/src/main/java/com/talhanation/bannermod/client/military/gui/war/PoliticalEntityCharterScreen.java new file mode 100644 index 00000000..b08f60f0 --- /dev/null +++ b/src/main/java/com/talhanation/bannermod/client/military/gui/war/PoliticalEntityCharterScreen.java @@ -0,0 +1,131 @@ +package com.talhanation.bannermod.client.military.gui.war; + +import com.talhanation.bannermod.client.military.gui.MilitaryGuiStyle; +import com.talhanation.bannermod.client.military.gui.component.RecruitsMultiLineEditBox; +import com.talhanation.bannermod.war.registry.PoliticalRegistryValidation; +import net.minecraft.client.gui.GuiGraphics; +import net.minecraft.client.gui.components.Button; +import net.minecraft.client.gui.screens.Screen; +import net.minecraft.network.chat.Component; + +import java.util.List; +import java.util.function.Consumer; + +public class PoliticalEntityCharterScreen extends Screen { + private static final int W = 320; + private static final int H = 224; + + private final Screen parent; + private final Consumer onSubmit; + private final String initialValue; + private int guiLeft; + private int guiTop; + private RecruitsMultiLineEditBox charterBox; + + public PoliticalEntityCharterScreen(Screen parent, String initialValue, Consumer onSubmit) { + super(Component.translatable("gui.bannermod.states.dialog.charter.title")); + this.parent = parent; + this.onSubmit = onSubmit; + this.initialValue = initialValue == null ? "" : initialValue; + } + + @Override + protected void init() { + super.init(); + this.guiLeft = (this.width - W) / 2; + this.guiTop = Math.max(8, (this.height - H) / 2); + + this.charterBox = new RecruitsMultiLineEditBox(font, guiLeft + 14, guiTop + 68, W - 28, 100, Component.empty(), Component.empty()); + this.charterBox.setValue(this.initialValue); + this.charterBox.setEnableEditing(true); + this.charterBox.setCharacterLimit(PoliticalRegistryValidation.MAX_CHARTER_LENGTH); + this.charterBox.setFocused(true); + addRenderableWidget(this.charterBox); + setInitialFocus(this.charterBox); + + addRenderableWidget(Button.builder(Component.translatable("gui.bannermod.common.submit"), button -> submit()) + .bounds(guiLeft + 14, guiTop + H - 28, 88, 20) + .build()); + addRenderableWidget(Button.builder(Component.translatable("gui.bannermod.states.dialog.charter.clear"), button -> clearDraft()) + .bounds(guiLeft + 116, guiTop + H - 28, 88, 20) + .build()); + addRenderableWidget(Button.builder(Component.translatable("gui.bannermod.common.back"), button -> onClose()) + .bounds(guiLeft + W - 102, guiTop + H - 28, 88, 20) + .build()); + } + + private void submit() { + this.onSubmit.accept(this.charterBox.getValue().trim()); + onClose(); + } + + private void clearDraft() { + this.charterBox.setValue(""); + this.charterBox.setFocused(true); + } + + @Override + public void render(GuiGraphics graphics, int mouseX, int mouseY, float partialTick) { + graphics.fill(0, 0, width, height, 0x66000000); + MilitaryGuiStyle.parchmentPanel(graphics, guiLeft, guiTop, W, H); + MilitaryGuiStyle.titleStrip(graphics, guiLeft + 8, guiTop + 8, W - 16, 14); + MilitaryGuiStyle.parchmentInset(graphics, guiLeft + 10, guiTop + 24, W - 20, 36); + MilitaryGuiStyle.insetPanel(graphics, guiLeft + 12, guiTop + 66, W - 24, 104); + + MilitaryGuiStyle.drawCenteredTitle(graphics, font, title, guiLeft, guiTop + 11, W); + drawWrapped(graphics, + Component.translatable("gui.bannermod.states.dialog.charter.subtitle"), + guiLeft + 16, + guiTop + 30, + W - 32, + MilitaryGuiStyle.TEXT_DARK); + graphics.drawString(font, + Component.translatable("gui.bannermod.states.dialog.charter.prompt", PoliticalRegistryValidation.MAX_CHARTER_LENGTH), + guiLeft + 14, + guiTop + 58, + MilitaryGuiStyle.TEXT_MUTED, + false); + + if (this.charterBox != null) { + String count = Component.translatable( + "gui.bannermod.states.dialog.charter.count", + this.charterBox.getValue().length(), + PoliticalRegistryValidation.MAX_CHARTER_LENGTH).getString(); + graphics.drawString(font, count, guiLeft + 14, guiTop + 176, MilitaryGuiStyle.TEXT_MUTED, false); + Component preview = this.charterBox.getValue().isBlank() + ? Component.translatable("gui.bannermod.states.dialog.charter.empty") + : Component.translatable("gui.bannermod.states.dialog.charter.preview", this.charterBox.getValue().replaceAll("\\s+", " ").trim()); + graphics.drawString(font, + font.plainSubstrByWidth(preview.getString(), W - 32), + guiLeft + 14, + guiTop + 188, + this.charterBox.getValue().isBlank() ? MilitaryGuiStyle.TEXT_MUTED : MilitaryGuiStyle.TEXT_DARK, + false); + } + + super.render(graphics, mouseX, mouseY, partialTick); + } + + private int drawWrapped(GuiGraphics graphics, Component text, int x, int y, int width, int color) { + List lines = this.font.split(text, width); + for (net.minecraft.util.FormattedCharSequence line : lines) { + graphics.drawString(this.font, line, x, y, color, false); + y += 10; + } + return y; + } + + @Override + public void onClose() { + if (parent != null) { + this.minecraft.setScreen(parent); + } else { + super.onClose(); + } + } + + @Override + public boolean isPauseScreen() { + return false; + } +} diff --git a/src/main/java/com/talhanation/bannermod/client/military/gui/war/PoliticalEntityCoLeaderPickerScreen.java b/src/main/java/com/talhanation/bannermod/client/military/gui/war/PoliticalEntityCoLeaderPickerScreen.java new file mode 100644 index 00000000..dbee15a6 --- /dev/null +++ b/src/main/java/com/talhanation/bannermod/client/military/gui/war/PoliticalEntityCoLeaderPickerScreen.java @@ -0,0 +1,226 @@ +package com.talhanation.bannermod.client.military.gui.war; + +import com.talhanation.bannermod.bootstrap.BannerModMain; +import com.talhanation.bannermod.client.military.ClientManager; +import com.talhanation.bannermod.client.military.gui.MilitaryGuiStyle; +import com.talhanation.bannermod.client.military.gui.widgets.ScrollDropDownMenu; +import com.talhanation.bannermod.network.messages.war.MessageUpdateCoLeader; +import com.talhanation.bannermod.persistence.military.RecruitsPlayerInfo; +import net.minecraft.client.gui.GuiGraphics; +import net.minecraft.client.gui.components.Button; +import net.minecraft.client.gui.screens.Screen; +import net.minecraft.network.chat.Component; +import net.minecraft.util.FastColor; + +import javax.annotation.Nullable; +import java.util.ArrayList; +import java.util.Comparator; +import java.util.List; +import java.util.UUID; + +public class PoliticalEntityCoLeaderPickerScreen extends Screen { + private static final int W = 292; + private static final int H = 172; + + private final Screen parent; + private final UUID entityId; + private final UUID leaderUuid; + private final List existingCoLeaders; + private final List candidates = new ArrayList<>(); + private int guiLeft; + private int guiTop; + private int lastOnlinePlayersVersion = -1; + private ScrollDropDownMenu playerDropdown; + private Button grantButton; + @Nullable + private RecruitsPlayerInfo selectedPlayer; + + public PoliticalEntityCoLeaderPickerScreen(Screen parent, UUID entityId, @Nullable UUID leaderUuid, List existingCoLeaders) { + super(Component.translatable("gui.bannermod.states.dialog.co_leader_add.title")); + this.parent = parent; + this.entityId = entityId; + this.leaderUuid = leaderUuid; + this.existingCoLeaders = existingCoLeaders == null ? List.of() : List.copyOf(existingCoLeaders); + } + + @Override + protected void init() { + super.init(); + this.guiLeft = (this.width - W) / 2; + this.guiTop = Math.max(8, (this.height - H) / 2); + rebuildCandidates(); + + this.playerDropdown = new ScrollDropDownMenu<>(this.selectedPlayer, guiLeft + 16, guiTop + 72, W - 32, 20, this.candidates, + this::displayName, + selected -> { + this.selectedPlayer = selected; + updateButtons(); + }); + this.playerDropdown.setBgFill(FastColor.ARGB32.color(255, 66, 50, 34)); + this.playerDropdown.setBgFillHovered(FastColor.ARGB32.color(255, 104, 79, 54)); + this.playerDropdown.setBgFillSelected(FastColor.ARGB32.color(255, 50, 37, 24)); + this.playerDropdown.setDisplayColor(MilitaryGuiStyle.TEXT); + this.playerDropdown.setOptionTextColor(MilitaryGuiStyle.TEXT); + addRenderableWidget(this.playerDropdown); + + this.grantButton = addRenderableWidget(Button.builder(Component.translatable("gui.bannermod.states.dialog.co_leader.select"), button -> submitSelected()) + .bounds(guiLeft + 16, guiTop + H - 28, 82, 20) + .build()); + addRenderableWidget(Button.builder(Component.translatable("gui.bannermod.states.dialog.co_leader.manual"), button -> openManualEntry()) + .bounds(guiLeft + 105, guiTop + H - 28, 88, 20) + .build()); + addRenderableWidget(Button.builder(Component.translatable("gui.bannermod.common.back"), button -> onClose()) + .bounds(guiLeft + W - 98, guiTop + H - 28, 82, 20) + .build()); + updateButtons(); + } + + private void rebuildCandidates() { + this.candidates.clear(); + this.lastOnlinePlayersVersion = ClientManager.onlinePlayersVersion; + for (RecruitsPlayerInfo playerInfo : ClientManager.onlinePlayers) { + if (playerInfo == null || playerInfo.getUUID() == null) { + continue; + } + if (playerInfo.getUUID().equals(this.leaderUuid) || this.existingCoLeaders.contains(playerInfo.getUUID())) { + continue; + } + this.candidates.add(copyInfo(playerInfo)); + } + this.candidates.sort(Comparator.comparing(this::displayName, String.CASE_INSENSITIVE_ORDER)); + if (this.selectedPlayer != null && this.selectedPlayer.getUUID() != null) { + this.selectedPlayer = this.candidates.stream() + .filter(candidate -> candidate.getUUID().equals(this.selectedPlayer.getUUID())) + .findFirst() + .orElse(null); + } + } + + @Override + public void tick() { + super.tick(); + if (this.lastOnlinePlayersVersion != ClientManager.onlinePlayersVersion) { + this.init(); + } + } + + private void submitSelected() { + if (this.selectedPlayer == null || this.selectedPlayer.getUUID() == null) { + return; + } + BannerModMain.SIMPLE_CHANNEL.sendToServer(new MessageUpdateCoLeader(this.entityId, this.selectedPlayer.getUUID().toString(), true)); + this.minecraft.setScreen(this.parent); + } + + private void openManualEntry() { + this.minecraft.setScreen(new PoliticalEntityNameInputScreen( + this.parent, + Component.translatable("gui.bannermod.states.dialog.co_leader_add.title"), + Component.translatable("gui.bannermod.states.dialog.co_leader.prompt"), + "", + value -> BannerModMain.SIMPLE_CHANNEL.sendToServer(new MessageUpdateCoLeader(this.entityId, value, true)), + 36, + false + )); + } + + private void updateButtons() { + if (this.grantButton != null) { + boolean hasSelection = this.selectedPlayer != null && this.selectedPlayer.getUUID() != null; + this.grantButton.active = hasSelection; + this.grantButton.setTooltip(hasSelection ? net.minecraft.client.gui.components.Tooltip.create(Component.translatable("gui.bannermod.states.dialog.co_leader.select.tooltip")) + : net.minecraft.client.gui.components.Tooltip.create(Component.translatable("gui.bannermod.states.dialog.co_leader.empty"))); + } + } + + @Override + public void render(GuiGraphics graphics, int mouseX, int mouseY, float partialTick) { + graphics.fill(0, 0, width, height, 0x66000000); + MilitaryGuiStyle.parchmentPanel(graphics, guiLeft, guiTop, W, H); + MilitaryGuiStyle.titleStrip(graphics, guiLeft + 8, guiTop + 8, W - 16, 14); + MilitaryGuiStyle.parchmentInset(graphics, guiLeft + 10, guiTop + 24, W - 20, 34); + MilitaryGuiStyle.insetPanel(graphics, guiLeft + 14, guiTop + 70, W - 28, 24); + + MilitaryGuiStyle.drawCenteredTitle(graphics, font, title, guiLeft, guiTop + 11, W); + drawWrapped(graphics, Component.translatable("gui.bannermod.states.dialog.co_leader.subtitle"), guiLeft + 16, guiTop + 32, W - 32, MilitaryGuiStyle.TEXT_DARK); + graphics.drawString(font, Component.translatable("gui.bannermod.states.dialog.co_leader.online"), guiLeft + 16, guiTop + 60, MilitaryGuiStyle.TEXT_MUTED, false); + drawWrapped(graphics, + candidates.isEmpty() + ? Component.translatable("gui.bannermod.states.dialog.co_leader.empty") + : Component.translatable("gui.bannermod.states.dialog.co_leader.selected", displayName(this.selectedPlayer)), + guiLeft + 16, + guiTop + 104, + W - 32, + MilitaryGuiStyle.TEXT_MUTED); + + super.render(graphics, mouseX, mouseY, partialTick); + } + + private int drawWrapped(GuiGraphics graphics, Component text, int x, int y, int width, int color) { + List lines = this.font.split(text, width); + for (net.minecraft.util.FormattedCharSequence line : lines) { + graphics.drawString(this.font, line, x, y, color, false); + y += 10; + } + return y; + } + + @Override + public boolean mouseClicked(double mouseX, double mouseY, int button) { + if (button == 0 && this.playerDropdown != null && this.playerDropdown.isMouseOver(mouseX, mouseY)) { + this.playerDropdown.onMouseClick(mouseX, mouseY); + return true; + } + return super.mouseClicked(mouseX, mouseY, button); + } + + @Override + public void mouseMoved(double mouseX, double mouseY) { + super.mouseMoved(mouseX, mouseY); + if (this.playerDropdown != null) { + this.playerDropdown.onMouseMove(mouseX, mouseY); + } + } + + @Override + public boolean mouseScrolled(double mouseX, double mouseY, double scrollX, double delta) { + if (this.playerDropdown != null && this.playerDropdown.isMouseOver(mouseX, mouseY) && this.playerDropdown.mouseScrolled(mouseX, mouseY, scrollX, delta)) { + return true; + } + return super.mouseScrolled(mouseX, mouseY, scrollX, delta); + } + + @Override + public boolean mouseReleased(double mouseX, double mouseY, int button) { + if (this.playerDropdown != null && this.playerDropdown.mouseReleased(mouseX, mouseY, button)) { + return true; + } + return super.mouseReleased(mouseX, mouseY, button); + } + + private String displayName(@Nullable RecruitsPlayerInfo playerInfo) { + if (playerInfo == null) { + return Component.translatable("gui.bannermod.common.none").getString(); + } + if (playerInfo.getName() != null && !playerInfo.getName().isBlank()) { + return playerInfo.getName(); + } + return playerInfo.getUUID() == null ? Component.translatable("gui.bannermod.common.none").getString() : playerInfo.getUUID().toString(); + } + + private RecruitsPlayerInfo copyInfo(RecruitsPlayerInfo playerInfo) { + RecruitsPlayerInfo copy = new RecruitsPlayerInfo(playerInfo.getUUID(), playerInfo.getName()); + copy.setOnline(playerInfo.isOnline()); + return copy; + } + + @Override + public void onClose() { + this.minecraft.setScreen(this.parent); + } + + @Override + public boolean isPauseScreen() { + return false; + } +} diff --git a/src/main/java/com/talhanation/bannermod/client/military/gui/war/PoliticalEntityListScreen.java b/src/main/java/com/talhanation/bannermod/client/military/gui/war/PoliticalEntityListScreen.java index e78e7dec..4bd98571 100644 --- a/src/main/java/com/talhanation/bannermod/client/military/gui/war/PoliticalEntityListScreen.java +++ b/src/main/java/com/talhanation/bannermod/client/military/gui/war/PoliticalEntityListScreen.java @@ -270,26 +270,32 @@ private void openColorDialog() { private void openCharterDialog() { if (this.selected == null) return; UUID id = this.selected.id(); - Minecraft.getInstance().setScreen(new PoliticalEntityNameInputScreen( + Minecraft.getInstance().setScreen(new PoliticalEntityCharterScreen( this, - text("gui.bannermod.states.dialog.charter.title"), - text("gui.bannermod.states.dialog.charter.prompt", PoliticalRegistryValidation.MAX_CHARTER_LENGTH), this.selected.charter(), - value -> sendCharter(id, value), - PoliticalRegistryValidation.MAX_CHARTER_LENGTH, - /* allowEmpty */ true + value -> sendCharter(id, value) + )); + } + + private void openAddCoLeaderDialog() { + if (this.selected == null) return; + Minecraft.getInstance().setScreen(new PoliticalEntityCoLeaderPickerScreen( + this, + this.selected.id(), + this.selected.leaderUuid(), + this.selected.coLeaderUuids() )); } - private void openCoLeaderDialog(boolean add) { + private void openRemoveCoLeaderDialog() { if (this.selected == null) return; UUID id = this.selected.id(); Minecraft.getInstance().setScreen(new PoliticalEntityNameInputScreen( this, - text(add ? "gui.bannermod.states.dialog.co_leader_add.title" : "gui.bannermod.states.dialog.co_leader_remove.title"), + text("gui.bannermod.states.dialog.co_leader_remove.title"), text("gui.bannermod.states.dialog.co_leader.prompt"), "", - value -> sendCoLeader(id, value, add), + value -> sendCoLeader(id, value, false), 36, false )); @@ -346,9 +352,9 @@ private List buildManageEntries() { entries.add(new ContextMenuEntry(text("gui.bannermod.states.charter").getString(), this::openCharterDialog, canAct)); entries.add(new ContextMenuEntry(text("gui.bannermod.states.add_co_leader").getString(), - () -> openCoLeaderDialog(true), leader)); + this::openAddCoLeaderDialog, leader)); entries.add(new ContextMenuEntry(text("gui.bannermod.states.remove_co_leader").getString(), - () -> openCoLeaderDialog(false), canRemoveCoLeader)); + this::openRemoveCoLeaderDialog, canRemoveCoLeader)); entries.add(new ContextMenuEntry(text("gui.bannermod.states.promote_state").getString(), this::promoteToState, canPromote)); return entries; @@ -433,6 +439,7 @@ private void renderDetails(GuiGraphics graphics) { text("gui.bannermod.states.detail.co_leader_authority", text(selected.governmentForm().coLeadersShareAuthority() ? "gui.bannermod.states.co_authority.active" : "gui.bannermod.states.co_authority.locked").getString()).getString(), text("gui.bannermod.states.detail.capital", selected.capitalPos() == null ? text("gui.bannermod.common.none").getString() : selected.capitalPos().toShortString()).getString(), text("gui.bannermod.states.detail.color", selected.color().isBlank() ? text("gui.bannermod.common.none").getString() : selected.color()).getString(), + text("gui.bannermod.states.detail.charter", charterSummary(selected)).getString(), text("gui.bannermod.states.detail.region", selected.homeRegion().isBlank() ? text("gui.bannermod.common.none").getString() : selected.homeRegion()).getString(), text("gui.bannermod.states.detail.wars", involvedWarCount(selected)).getString() }; @@ -548,6 +555,13 @@ private String coLeaderSummary(PoliticalEntityRecord entity) { return String.join(", ", names) + suffix; } + private String charterSummary(PoliticalEntityRecord entity) { + if (entity.charter().isBlank()) { + return text("gui.bannermod.common.none").getString(); + } + return entity.charter().replaceAll("\\s+", " ").trim(); + } + private static int involvedWarCount(PoliticalEntityRecord entity) { int count = 0; for (var war : WarClientState.wars()) { diff --git a/src/main/resources/assets/bannermod/lang/en_us.json b/src/main/resources/assets/bannermod/lang/en_us.json index b2964f62..c1e8f962 100644 --- a/src/main/resources/assets/bannermod/lang/en_us.json +++ b/src/main/resources/assets/bannermod/lang/en_us.json @@ -2505,11 +2505,23 @@ "gui.bannermod.states.charter": "Charter", "gui.bannermod.states.dialog.charter.prompt": "Charter text (max %s chars; empty to clear):", "gui.bannermod.states.dialog.charter.title": "Realm charter", + "gui.bannermod.states.dialog.charter.subtitle": "Write a public charter or RP description for this realm. It is shown in realm details and the realm info page.", + "gui.bannermod.states.dialog.charter.clear": "Clear draft", + "gui.bannermod.states.dialog.charter.count": "%s / %s chars", + "gui.bannermod.states.dialog.charter.empty": "No charter yet. Leave the field blank and submit to clear it on the server.", + "gui.bannermod.states.dialog.charter.preview": "Preview: %s", "gui.bannermod.states.dialog.co_leader.prompt": "Player name or UUID:", + "gui.bannermod.states.dialog.co_leader.subtitle": "Pick an online player here, or use manual entry for an offline nickname or UUID.", + "gui.bannermod.states.dialog.co_leader.online": "Online player list:", + "gui.bannermod.states.dialog.co_leader.empty": "No online candidates yet. Use manual entry for an offline player.", + "gui.bannermod.states.dialog.co_leader.selected": "Selected: %s", + "gui.bannermod.states.dialog.co_leader.select": "Grant seat", + "gui.bannermod.states.dialog.co_leader.select.tooltip": "Grant this player a co-leader seat.", + "gui.bannermod.states.dialog.co_leader.manual": "Manual entry", "gui.bannermod.states.dialog.co_leader_add.title": "Add co-leader", "gui.bannermod.states.dialog.co_leader_remove.title": "Remove co-leader", "gui.bannermod.states.co_leader.state_not_found": "Cannot update co-leader: state not found.", - "gui.bannermod.states.co_leader.player_not_found": "Player not found. Use an online nickname or a known UUID.", + "gui.bannermod.states.co_leader.player_not_found": "Player not found. Use a known nickname or UUID.", "gui.bannermod.states.co_leader.no_change": "Co-leader update did not change the state.", "gui.bannermod.states.co_leader.added": "%s added to co-leaders for %s.", "gui.bannermod.states.co_leader.removed": "%s removed from co-leaders for %s.", @@ -2535,6 +2547,7 @@ "gui.bannermod.states.menu.manage": "Manage state", "gui.bannermod.states.detail": "Realm Detail", "gui.bannermod.states.detail.capital": "Capital: %s", + "gui.bannermod.states.detail.charter": "Charter: %s", "gui.bannermod.states.detail.co_leader_authority": "Co-leader authority: %s", "gui.bannermod.states.detail.co_leaders": "Co-leaders: %s", "gui.bannermod.states.detail.color": "Color: %s", diff --git a/src/main/resources/assets/bannermod/lang/ru_ru.json b/src/main/resources/assets/bannermod/lang/ru_ru.json index 9ca9da9e..6598a884 100644 --- a/src/main/resources/assets/bannermod/lang/ru_ru.json +++ b/src/main/resources/assets/bannermod/lang/ru_ru.json @@ -2413,11 +2413,23 @@ "gui.bannermod.states.charter": "Хартия", "gui.bannermod.states.dialog.charter.prompt": "Текст хартии (макс. %s символов; пусто, чтобы очистить):", "gui.bannermod.states.dialog.charter.title": "Хартия владения", + "gui.bannermod.states.dialog.charter.subtitle": "Напиши публичную хартию или RP-описание владения. Оно показывается в деталях владения и на странице подробной информации.", + "gui.bannermod.states.dialog.charter.clear": "Очистить черновик", + "gui.bannermod.states.dialog.charter.count": "%s / %s символов", + "gui.bannermod.states.dialog.charter.empty": "Хартия пока не задана. Оставь поле пустым и нажми подтвердить, чтобы очистить ее на сервере.", + "gui.bannermod.states.dialog.charter.preview": "Предпросмотр: %s", "gui.bannermod.states.dialog.co_leader.prompt": "Ник или UUID игрока:", + "gui.bannermod.states.dialog.co_leader.subtitle": "Выбери здесь онлайн-игрока или используй ручной ввод для оффлайн-ника или UUID.", + "gui.bannermod.states.dialog.co_leader.online": "Список онлайн-игроков:", + "gui.bannermod.states.dialog.co_leader.empty": "Подходящих онлайн-игроков пока нет. Для оффлайн-игрока используй ручной ввод.", + "gui.bannermod.states.dialog.co_leader.selected": "Выбран: %s", + "gui.bannermod.states.dialog.co_leader.select": "Выдать место", + "gui.bannermod.states.dialog.co_leader.select.tooltip": "Выдать этому игроку место со-лидера.", + "gui.bannermod.states.dialog.co_leader.manual": "Ручной ввод", "gui.bannermod.states.dialog.co_leader_add.title": "Добавить со-лидера", "gui.bannermod.states.dialog.co_leader_remove.title": "Удалить со-лидера", "gui.bannermod.states.co_leader.state_not_found": "Не удалось обновить со-лидера: владение не найдено.", - "gui.bannermod.states.co_leader.player_not_found": "Игрок не найден. Используй ник онлайн-игрока или известный UUID.", + "gui.bannermod.states.co_leader.player_not_found": "Игрок не найден. Используй известный ник или UUID.", "gui.bannermod.states.co_leader.no_change": "Список со-лидеров не изменился.", "gui.bannermod.states.co_leader.added": "%s добавлен в со-лидеры владения %s.", "gui.bannermod.states.co_leader.removed": "%s удален из со-лидеров владения %s.", @@ -2443,6 +2455,7 @@ "gui.bannermod.states.menu.manage": "Управление государством", "gui.bannermod.states.detail": "Подробности владения", "gui.bannermod.states.detail.capital": "Столица: %s", + "gui.bannermod.states.detail.charter": "Хартия: %s", "gui.bannermod.states.detail.co_leader_authority": "Полномочия со-лидеров: %s", "gui.bannermod.states.detail.co_leaders": "Со-лидеры: %s", "gui.bannermod.states.detail.color": "Цвет: %s", diff --git a/src/test/java/com/talhanation/bannermod/client/military/gui/PoliticalEntityCharterAndCoLeaderUiVerificationTest.java b/src/test/java/com/talhanation/bannermod/client/military/gui/PoliticalEntityCharterAndCoLeaderUiVerificationTest.java new file mode 100644 index 00000000..6e7e7be0 --- /dev/null +++ b/src/test/java/com/talhanation/bannermod/client/military/gui/PoliticalEntityCharterAndCoLeaderUiVerificationTest.java @@ -0,0 +1,41 @@ +package com.talhanation.bannermod.client.military.gui; + +import org.junit.jupiter.api.Test; + +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; + +import static org.junit.jupiter.api.Assertions.assertTrue; + +class PoliticalEntityCharterAndCoLeaderUiVerificationTest { + private static final Path ROOT = Path.of(""); + + @Test + void stateScreenUsesDedicatedCharterEditorAndPlayerPicker() throws IOException { + String listScreen = read("src/main/java/com/talhanation/bannermod/client/military/gui/war/PoliticalEntityListScreen.java"); + String charterScreen = read("src/main/java/com/talhanation/bannermod/client/military/gui/war/PoliticalEntityCharterScreen.java"); + String coLeaderPickerScreen = read("src/main/java/com/talhanation/bannermod/client/military/gui/war/PoliticalEntityCoLeaderPickerScreen.java"); + String enLang = read("src/main/resources/assets/bannermod/lang/en_us.json"); + String ruLang = read("src/main/resources/assets/bannermod/lang/ru_ru.json"); + + assertTrue(listScreen.contains("new PoliticalEntityCharterScreen(")); + assertTrue(listScreen.contains("new PoliticalEntityCoLeaderPickerScreen(")); + assertTrue(listScreen.contains("gui.bannermod.states.detail.charter")); + assertTrue(charterScreen.contains("RecruitsMultiLineEditBox")); + assertTrue(charterScreen.contains("gui.bannermod.states.dialog.charter.subtitle")); + assertTrue(coLeaderPickerScreen.contains("MessageUpdateCoLeader")); + assertTrue(coLeaderPickerScreen.contains("ClientManager.onlinePlayersVersion")); + assertTrue(coLeaderPickerScreen.contains("this.init();")); + assertTrue(coLeaderPickerScreen.contains("drawWrapped(graphics, Component.translatable(\"gui.bannermod.states.dialog.co_leader.subtitle\")")); + assertTrue(coLeaderPickerScreen.contains("gui.bannermod.states.dialog.co_leader.manual")); + assertTrue(enLang.contains("gui.bannermod.states.dialog.co_leader.select.tooltip")); + assertTrue(enLang.contains("gui.bannermod.states.dialog.co_leader.manual")); + assertTrue(ruLang.contains("gui.bannermod.states.dialog.co_leader.select.tooltip")); + assertTrue(ruLang.contains("gui.bannermod.states.dialog.co_leader.manual")); + } + + private static String read(String relativePath) throws IOException { + return Files.readString(ROOT.resolve(relativePath)); + } +}