Compare commits
2
Commits
2d96d9077b
...
ea80f4a6f4
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
ea80f4a6f4 | ||
|
|
fa5f6003c5 |
+6
-1
@@ -8,7 +8,12 @@ Ce journal suit les **versions du modpack**. Lorsqu'un seul module change, sa ve
|
||||
|
||||
## Non publié
|
||||
|
||||
Aucun changement non publié.
|
||||
Issue [#169](https://git.botsu.net/koka/sanctuary/issues/169) — catalogue du Shop.
|
||||
|
||||
- ajoute une barre de recherche dans l’onglet catalogue, avec repli des accents et filtrage local ;
|
||||
- permet de choisir la grille du catalogue : **3×3**, **4×4** ou **5×5** maximum, mémorisée entre les sessions ;
|
||||
- affiche le numéro de page du catalogue (`2/5`) à côté de la navigation ;
|
||||
- conserve la grille 3×3 et les neuf emplacements des ventes flash.
|
||||
|
||||
## `26.2.0-alpha.242`
|
||||
|
||||
|
||||
+11
-1
@@ -100,6 +100,14 @@ tasks.register("shopHeaderLayoutSmoke", JavaExec) {
|
||||
mainClass = "fr.koka99cab.sanctuary26.sanctuary.client.ShopDailyHeaderLayoutSmoke"
|
||||
}
|
||||
|
||||
tasks.register("shopCatalogBrowseSmoke", JavaExec) {
|
||||
group = "verification"
|
||||
description = "Checks catalogue search folding, 3x3/4x4/5x5 grids and filtered pagination."
|
||||
dependsOn tasks.named("testClasses")
|
||||
classpath = sourceSets.test.runtimeClasspath
|
||||
mainClass = "fr.koka99cab.sanctuary26.sanctuary.shop.ShopCatalogBrowseSmoke"
|
||||
}
|
||||
|
||||
tasks.register("zombieNamesSmoke", JavaExec) {
|
||||
group = "verification"
|
||||
description = "Checks deterministic Xbox-360-style names for every vanilla Zombie subtype."
|
||||
@@ -360,7 +368,8 @@ tasks.register("verifySanctuary") {
|
||||
tasks.named("shopDeliveryTimingSmoke"), tasks.named("blackMarketModelSmoke"),
|
||||
tasks.named("questBoardModelSmoke"), tasks.named("questTrackerModelSmoke"),
|
||||
tasks.named("questIdSuggestionsSmoke"),
|
||||
tasks.named("shopHeaderLayoutSmoke"), tasks.named("zombieNamesSmoke"),
|
||||
tasks.named("shopHeaderLayoutSmoke"), tasks.named("shopCatalogBrowseSmoke"),
|
||||
tasks.named("zombieNamesSmoke"),
|
||||
tasks.named("capeJoinSyncSmoke"),
|
||||
tasks.named("inventorySortSmoke"),
|
||||
tasks.named("verifyBlackMarket")
|
||||
@@ -3706,6 +3715,7 @@ tasks.named("check") {
|
||||
dependsOn tasks.named("questTrackerModelSmoke")
|
||||
dependsOn tasks.named("questTrackerDisplaySmoke")
|
||||
dependsOn tasks.named("questIdSuggestionsSmoke")
|
||||
dependsOn tasks.named("shopCatalogBrowseSmoke")
|
||||
dependsOn tasks.named("alphaIndevGeneratorSmoke")
|
||||
dependsOn tasks.named("capeJoinSyncSmoke")
|
||||
dependsOn tasks.named("capeSyncPayloadSmoke")
|
||||
|
||||
@@ -116,6 +116,7 @@ public final class SanctuaryClient implements ClientModInitializer {
|
||||
));
|
||||
SanctuaryAtlasOptions.load();
|
||||
SanctuaryClientQuests.load();
|
||||
SanctuaryShopOptions.load();
|
||||
SanctuaryBulkActionsClient.initialize();
|
||||
SanctuaryHealthPreview.register();
|
||||
CelestialSpyglassSelection.register();
|
||||
|
||||
+71
@@ -0,0 +1,71 @@
|
||||
package fr.koka99cab.sanctuary26.sanctuary.client;
|
||||
|
||||
import com.google.gson.Gson;
|
||||
import com.google.gson.GsonBuilder;
|
||||
import fr.koka99cab.sanctuary26.sanctuary.SanctuaryMod;
|
||||
import fr.koka99cab.sanctuary26.sanctuary.shop.ShopCatalogBrowse;
|
||||
import java.io.IOException;
|
||||
import java.io.Reader;
|
||||
import java.io.Writer;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import net.fabricmc.loader.api.FabricLoader;
|
||||
|
||||
/** Persisted catalogue grid preference for the Shop screen. */
|
||||
final class SanctuaryShopOptions {
|
||||
private static final Gson GSON = new GsonBuilder().setPrettyPrinting().create();
|
||||
private static final Path FILE = FabricLoader.getInstance().getConfigDir().resolve("sanctuary-shop.json");
|
||||
private static Settings settings = new Settings();
|
||||
private static boolean loaded;
|
||||
|
||||
private SanctuaryShopOptions() {
|
||||
}
|
||||
|
||||
static void load() {
|
||||
if (loaded) return;
|
||||
loaded = true;
|
||||
if (!Files.isRegularFile(FILE)) return;
|
||||
try (Reader reader = Files.newBufferedReader(FILE, StandardCharsets.UTF_8)) {
|
||||
Settings decoded = GSON.fromJson(reader, Settings.class);
|
||||
if (decoded != null) settings = decoded.normalized();
|
||||
} catch (IOException | RuntimeException exception) {
|
||||
SanctuaryMod.LOGGER.warn("[{}] Could not read Shop catalogue options.",
|
||||
SanctuaryMod.DISPLAY_NAME, exception);
|
||||
}
|
||||
}
|
||||
|
||||
static int catalogGrid() {
|
||||
load();
|
||||
return settings.catalogGrid;
|
||||
}
|
||||
|
||||
static void setCatalogGrid(int grid) {
|
||||
load();
|
||||
int next = ShopCatalogBrowse.clampGrid(grid);
|
||||
if (next == settings.catalogGrid) return;
|
||||
settings.catalogGrid = next;
|
||||
save();
|
||||
}
|
||||
|
||||
private static void save() {
|
||||
try {
|
||||
Files.createDirectories(FILE.getParent());
|
||||
try (Writer writer = Files.newBufferedWriter(FILE, StandardCharsets.UTF_8)) {
|
||||
GSON.toJson(settings, writer);
|
||||
}
|
||||
} catch (IOException exception) {
|
||||
SanctuaryMod.LOGGER.warn("[{}] Could not save Shop catalogue options.",
|
||||
SanctuaryMod.DISPLAY_NAME, exception);
|
||||
}
|
||||
}
|
||||
|
||||
private static final class Settings {
|
||||
int catalogGrid = ShopCatalogBrowse.DEFAULT_GRID;
|
||||
|
||||
private Settings normalized() {
|
||||
catalogGrid = ShopCatalogBrowse.clampGrid(catalogGrid);
|
||||
return this;
|
||||
}
|
||||
}
|
||||
}
|
||||
+161
-32
@@ -3,6 +3,7 @@ package fr.koka99cab.sanctuary26.sanctuary.client;
|
||||
import fr.koka99cab.sanctuary26.sanctuary.network.ShopActionPayload;
|
||||
import fr.koka99cab.sanctuary26.sanctuary.network.ShopRequestPayload;
|
||||
import fr.koka99cab.sanctuary26.sanctuary.network.ShopSnapshotPayload;
|
||||
import fr.koka99cab.sanctuary26.sanctuary.shop.ShopCatalogBrowse;
|
||||
import fr.koka99cab.sanctuary26.sanctuary.shop.ShopOffer;
|
||||
import fr.koka99cab.sanctuary26.sanctuary.shop.ShopProgression;
|
||||
import java.util.ArrayList;
|
||||
@@ -10,7 +11,11 @@ import java.util.List;
|
||||
import net.fabricmc.fabric.api.client.networking.v1.ClientPlayNetworking;
|
||||
import net.minecraft.client.gui.GuiGraphicsExtractor;
|
||||
import net.minecraft.client.gui.components.Button;
|
||||
import net.minecraft.client.gui.components.EditBox;
|
||||
import net.minecraft.client.gui.components.Tooltip;
|
||||
import net.minecraft.client.gui.screens.Screen;
|
||||
import net.minecraft.client.input.KeyEvent;
|
||||
import net.minecraft.client.input.MouseButtonEvent;
|
||||
import net.minecraft.core.registries.BuiltInRegistries;
|
||||
import net.minecraft.network.chat.Component;
|
||||
import net.minecraft.resources.Identifier;
|
||||
@@ -21,6 +26,8 @@ public final class SanctuaryShopScreen extends Screen {
|
||||
private static final int PAGE_SIZE = 9;
|
||||
private static final int GRID_COLUMNS = 3;
|
||||
private static final int GRID_ROWS = 3;
|
||||
private static final int TOOLBAR_Y = 48;
|
||||
private static final int PAGE_LABEL_WIDTH = 44;
|
||||
private final Screen parent;
|
||||
private final List<ActionHint> actionHints = new ArrayList<>();
|
||||
private ShopSnapshotPayload rendered;
|
||||
@@ -31,6 +38,9 @@ public final class SanctuaryShopScreen extends Screen {
|
||||
private long requestedRefreshForExpiry = Long.MIN_VALUE;
|
||||
private int page;
|
||||
private int renderedExperienceLevel = -1;
|
||||
private String catalogSearch = "";
|
||||
private boolean catalogSearchFocused;
|
||||
private EditBox catalogSearchBox;
|
||||
|
||||
public SanctuaryShopScreen(Screen parent) {
|
||||
super(Component.translatable("screen.sanctuary.shop.title"));
|
||||
@@ -41,6 +51,7 @@ public final class SanctuaryShopScreen extends Screen {
|
||||
rendered = SanctuaryClientShop.snapshot();
|
||||
renderedExperienceLevel = minecraft == null || minecraft.player == null ? -1 : minecraft.player.experienceLevel;
|
||||
actionHints.clear();
|
||||
catalogSearchBox = null;
|
||||
if (rendered == null || !rendered.operator()) {
|
||||
operatorPage = false;
|
||||
confirmCatalogReset = false;
|
||||
@@ -84,25 +95,16 @@ public final class SanctuaryShopScreen extends Screen {
|
||||
}).bounds(center - 100, 109, 200, 20).build());
|
||||
} else {
|
||||
List<ShopOffer> offers = visibleOffers();
|
||||
int maxPage = offers.isEmpty() ? 0 : (offers.size() - 1) / PAGE_SIZE;
|
||||
page = Math.min(page, maxPage);
|
||||
int start = page * PAGE_SIZE;
|
||||
int end = Math.min(offers.size(), start + PAGE_SIZE);
|
||||
int pageSize = pageSize();
|
||||
page = ShopCatalogBrowse.clampPage(page, offers.size(), pageSize);
|
||||
int start = page * pageSize;
|
||||
int end = Math.min(offers.size(), start + pageSize);
|
||||
GridLayout layout = gridLayout();
|
||||
for (int index = start; index < end; index++) addOfferButtons(offers.get(index), index - start, layout);
|
||||
|
||||
if (!catalogPage && rendered != null) addLockedOfferButtons(layout);
|
||||
|
||||
if (catalogPage) {
|
||||
Button previous = addRenderableWidget(Button.builder(Component.literal("<"), button -> {
|
||||
page = Math.max(0, page - 1); rebuildWidgets();
|
||||
}).bounds(layout.left(), 48, 32, 18).build());
|
||||
Button next = addRenderableWidget(Button.builder(Component.literal(">"), button -> {
|
||||
page++; rebuildWidgets();
|
||||
}).bounds(layout.left() + 36, 48, 32, 18).build());
|
||||
previous.active = page > 0;
|
||||
next.active = (page + 1) * PAGE_SIZE < offers.size();
|
||||
}
|
||||
if (catalogPage) addCatalogToolbar(layout, offers.size());
|
||||
}
|
||||
if (!requested && ClientPlayNetworking.canSend(ShopRequestPayload.ID)) {
|
||||
requested = true;
|
||||
@@ -110,6 +112,40 @@ public final class SanctuaryShopScreen extends Screen {
|
||||
}
|
||||
}
|
||||
|
||||
private void addCatalogToolbar(GridLayout layout, int offerCount) {
|
||||
int pageSize = pageSize();
|
||||
Button previous = addRenderableWidget(Button.builder(Component.literal("<"), button -> {
|
||||
page = Math.max(0, page - 1); rebuildWidgets();
|
||||
}).bounds(layout.left(), TOOLBAR_Y, 32, 18).build());
|
||||
Button next = addRenderableWidget(Button.builder(Component.literal(">"), button -> {
|
||||
page++; rebuildWidgets();
|
||||
}).bounds(layout.left() + 36, TOOLBAR_Y, 32, 18).build());
|
||||
previous.active = page > 0;
|
||||
next.active = (page + 1) * pageSize < offerCount;
|
||||
|
||||
int gridControlsWidth = 34 * 3 + 4 * 2;
|
||||
int searchX = layout.left() + 72 + PAGE_LABEL_WIDTH + 4;
|
||||
int searchWidth = Math.max(72, layout.width() - 72 - PAGE_LABEL_WIDTH - 4 - gridControlsWidth - 6);
|
||||
catalogSearchBox = addRenderableWidget(new EditBox(font, searchX, TOOLBAR_Y, searchWidth, 18,
|
||||
Component.translatable("screen.sanctuary.shop.search")));
|
||||
catalogSearchBox.setMaxLength(ShopCatalogBrowse.MAX_SEARCH_LENGTH);
|
||||
catalogSearchBox.setHint(Component.translatable("screen.sanctuary.shop.search_hint"));
|
||||
catalogSearchBox.setValue(catalogSearch);
|
||||
catalogSearchBox.setResponder(this::catalogSearchChanged);
|
||||
if (catalogSearchFocused) setInitialFocus(catalogSearchBox);
|
||||
|
||||
int gridX = searchX + searchWidth + 6;
|
||||
for (int grid = ShopCatalogBrowse.MIN_GRID; grid <= ShopCatalogBrowse.MAX_GRID; grid++) {
|
||||
int selected = grid;
|
||||
Button mode = addRenderableWidget(Button.builder(Component.literal(selected + "×" + selected),
|
||||
button -> setCatalogGrid(selected))
|
||||
.bounds(gridX + (selected - ShopCatalogBrowse.MIN_GRID) * 38, TOOLBAR_Y, 34, 18)
|
||||
.tooltip(Tooltip.create(Component.translatable("screen.sanctuary.shop.grid", selected, selected)))
|
||||
.build());
|
||||
mode.active = catalogGrid() != selected;
|
||||
}
|
||||
}
|
||||
|
||||
private void addOfferButtons(ShopOffer offer, int visibleIndex, GridLayout layout) {
|
||||
Card card = layout.card(visibleIndex);
|
||||
int actionY = card.y() + card.height() - 21;
|
||||
@@ -178,9 +214,11 @@ public final class SanctuaryShopScreen extends Screen {
|
||||
Component.translatable("screen.sanctuary.shop.slots_maximum", rendered.visibleOfferSlots(), PAGE_SIZE),
|
||||
layout.left() + Math.min(240, layout.width() / 2) / 2,
|
||||
ShopDailyHeaderLayout.PROGRESS_Y, 0xFF80E09A);
|
||||
if (catalogPage) drawCatalogPage(graphics, layout);
|
||||
}
|
||||
if (!operatorPage && !rendered.hasDeliveryBox()) graphics.centeredText(font,
|
||||
Component.translatable("screen.sanctuary.shop.no_box"), width / 2, 58, 0xFFFF7070);
|
||||
Component.translatable("screen.sanctuary.shop.no_box"), width / 2,
|
||||
catalogPage ? Math.max(68, height - 54) : 58, 0xFFFF7070);
|
||||
if (!rendered.status().isEmpty()) graphics.centeredText(font,
|
||||
Component.translatable("screen.sanctuary.shop.status." + rendered.status()), width / 2, Math.max(68, height - 41), 0xFF80E09A);
|
||||
if (operatorPage) {
|
||||
@@ -194,23 +232,66 @@ public final class SanctuaryShopScreen extends Screen {
|
||||
drawActionHints(graphics, mouseX, mouseY);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean keyPressed(KeyEvent event) {
|
||||
if (ShopCatalogBrowse.capturesInventoryKey(catalogSearchBox != null && catalogSearchBox.isFocused(),
|
||||
minecraft != null && minecraft.options.keyInventory.matches(event))) return true;
|
||||
return super.keyPressed(event);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean mouseClicked(MouseButtonEvent event, boolean doubleClick) {
|
||||
if (catalogSearchBox != null && catalogSearchBox.isFocused()
|
||||
&& !catalogSearchBox.isMouseOver(event.x(), event.y())) {
|
||||
catalogSearchFocused = false;
|
||||
setFocused(null);
|
||||
}
|
||||
return super.mouseClicked(event, doubleClick);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean mouseScrolled(double mouseX, double mouseY, double scrollX, double scrollY) {
|
||||
if (catalogPage && !operatorPage && scrollY != 0.0D) {
|
||||
int next = page + (scrollY < 0.0D ? 1 : -1);
|
||||
int clamped = ShopCatalogBrowse.clampPage(next, visibleOffers().size(), pageSize());
|
||||
if (clamped != page) {
|
||||
page = clamped;
|
||||
rebuildWidgets();
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return super.mouseScrolled(mouseX, mouseY, scrollX, scrollY);
|
||||
}
|
||||
|
||||
private void drawOffers(GuiGraphicsExtractor graphics, int mouseX, int mouseY, GridLayout layout) {
|
||||
List<ShopOffer> offers = visibleOffers();
|
||||
int start = Math.min(page * PAGE_SIZE, offers.size());
|
||||
int end = Math.min(offers.size(), start + PAGE_SIZE);
|
||||
int pageSize = pageSize();
|
||||
int start = Math.min(page * pageSize, offers.size());
|
||||
int end = Math.min(offers.size(), start + pageSize);
|
||||
for (int index = start; index < end; index++) {
|
||||
ShopOffer offer = offers.get(index); int visible = index - start;
|
||||
Card card = layout.card(visible);
|
||||
graphics.fill(card.x(), card.y(), card.x() + card.width(), card.y() + card.height(), 0x38000000);
|
||||
graphics.outline(card.x(), card.y(), card.width(), card.height(), 0x697F7F7F);
|
||||
ItemStack stack = offer.stack();
|
||||
drawLargeItem(graphics, stack, card.x() + 7, card.y() + 6);
|
||||
String displayName = font.plainSubstrByWidth(stack.getHoverName().getString(), Math.max(20, card.width() - 49));
|
||||
graphics.text(font, Component.literal(displayName), card.x() + 45, card.y() + 8, 0xFFFFFFFF);
|
||||
if (offer.count() > 1) graphics.text(font, Component.literal("×" + offer.count()),
|
||||
card.x() + 45, card.y() + 21, 0xFFB9B9B9);
|
||||
if (mouseX >= card.x() + 6 && mouseX < card.x() + 40
|
||||
&& mouseY >= card.y() + 5 && mouseY < card.y() + 39)
|
||||
boolean compact = card.width() < 90 || card.height() < 70;
|
||||
float itemScale = compact ? 1.0F : 2.0F;
|
||||
int iconSize = Math.round(16 * itemScale);
|
||||
int itemX = card.x() + (compact ? 4 : 7);
|
||||
int itemY = card.y() + (compact ? 4 : 6);
|
||||
drawScaledItem(graphics, stack, itemX, itemY, itemScale);
|
||||
int textX = itemX + iconSize + 4;
|
||||
int textWidth = Math.max(12, card.x() + card.width() - 4 - textX);
|
||||
graphics.text(font, Component.literal(font.plainSubstrByWidth(
|
||||
stack.getHoverName().getString(), textWidth)), textX, itemY + 2, 0xFFFFFFFF);
|
||||
if (offer.count() > 1) {
|
||||
int countY = compact ? itemY + 2 + font.lineHeight : itemY + 15;
|
||||
if (countY + font.lineHeight <= card.y() + card.height() - 22) {
|
||||
graphics.text(font, Component.literal("×" + offer.count()), textX, countY, 0xFFB9B9B9);
|
||||
}
|
||||
}
|
||||
if (mouseX >= card.x() && mouseX < card.x() + card.width()
|
||||
&& mouseY >= card.y() && mouseY < card.y() + card.height() - 21)
|
||||
graphics.setTooltipForNextFrame(font, stack, mouseX, mouseY);
|
||||
}
|
||||
if (!catalogPage && rendered != null) {
|
||||
@@ -223,7 +304,19 @@ public final class SanctuaryShopScreen extends Screen {
|
||||
card.x() + card.width() / 2, card.y() + card.height() / 2 - 23, 0xFF8A8A8A);
|
||||
}
|
||||
}
|
||||
if (offers.isEmpty()) graphics.centeredText(font, Component.translatable("screen.sanctuary.shop.catalog_empty"), width / 2, 112, 0xFF555555);
|
||||
if (offers.isEmpty()) graphics.centeredText(font, Component.translatable(catalogPage
|
||||
&& !ShopCatalogBrowse.normalize(catalogSearch).isEmpty()
|
||||
? "screen.sanctuary.shop.catalog_no_results"
|
||||
: "screen.sanctuary.shop.catalog_empty"), width / 2, 112, 0xFF555555);
|
||||
}
|
||||
|
||||
private void drawCatalogPage(GuiGraphicsExtractor graphics, GridLayout layout) {
|
||||
int itemCount = visibleOffers().size();
|
||||
int pageSize = pageSize();
|
||||
graphics.centeredText(font, Component.translatable("screen.sanctuary.shop.page",
|
||||
ShopCatalogBrowse.clampPage(page, itemCount, pageSize) + 1,
|
||||
ShopCatalogBrowse.pageCount(itemCount, pageSize)),
|
||||
layout.left() + 72 + PAGE_LABEL_WIDTH / 2, TOOLBAR_Y + 5, 0xFFD0D0D0);
|
||||
}
|
||||
|
||||
private void drawActionHints(GuiGraphicsExtractor graphics, int mouseX, int mouseY) {
|
||||
@@ -243,9 +336,13 @@ public final class SanctuaryShopScreen extends Screen {
|
||||
}
|
||||
|
||||
private static void drawLargeItem(GuiGraphicsExtractor graphics, ItemStack stack, int x, int y) {
|
||||
drawScaledItem(graphics, stack, x, y, 2.0F);
|
||||
}
|
||||
|
||||
private static void drawScaledItem(GuiGraphicsExtractor graphics, ItemStack stack, int x, int y, float scale) {
|
||||
graphics.pose().pushMatrix();
|
||||
graphics.pose().translate(x, y);
|
||||
graphics.pose().scale(2.0F, 2.0F);
|
||||
graphics.pose().scale(scale, scale);
|
||||
graphics.item(stack, 0, 0);
|
||||
graphics.pose().popMatrix();
|
||||
}
|
||||
@@ -290,14 +387,44 @@ public final class SanctuaryShopScreen extends Screen {
|
||||
|
||||
private List<ShopOffer> visibleOffers() {
|
||||
if (rendered == null || operatorPage) return List.of();
|
||||
return catalogPage ? rendered.catalogOffers() : rendered.dailyOffers();
|
||||
if (!catalogPage) return rendered.dailyOffers();
|
||||
return ShopCatalogBrowse.filter(rendered.catalogOffers(), catalogSearch, ShopOffer::itemId,
|
||||
offer -> offer.stack().getHoverName().getString());
|
||||
}
|
||||
|
||||
private int catalogGrid() {
|
||||
return SanctuaryShopOptions.catalogGrid();
|
||||
}
|
||||
|
||||
private int pageSize() {
|
||||
return catalogPage ? ShopCatalogBrowse.pageSize(catalogGrid()) : PAGE_SIZE;
|
||||
}
|
||||
|
||||
private void catalogSearchChanged(String value) {
|
||||
if (value.equals(catalogSearch)) return;
|
||||
catalogSearch = value;
|
||||
catalogSearchFocused = true;
|
||||
page = 0;
|
||||
rebuildWidgets();
|
||||
}
|
||||
|
||||
private void setCatalogGrid(int requested) {
|
||||
int next = ShopCatalogBrowse.clampGrid(requested);
|
||||
if (next == catalogGrid()) return;
|
||||
SanctuaryShopOptions.setCatalogGrid(next);
|
||||
page = 0;
|
||||
rebuildWidgets();
|
||||
}
|
||||
|
||||
private GridLayout gridLayout() {
|
||||
int panelWidth = Math.max(240, Math.min(720, width - 12));
|
||||
int left = width / 2 - panelWidth / 2;
|
||||
int top = ShopDailyHeaderLayout.GRID_TOP;
|
||||
int availableHeight = Math.max(174, height - top - 36);
|
||||
return new GridLayout(left, top, panelWidth, availableHeight);
|
||||
int grid = catalogGrid();
|
||||
int columns = catalogPage ? grid : GRID_COLUMNS;
|
||||
int rows = catalogPage ? grid : GRID_ROWS;
|
||||
return new GridLayout(left, top, panelWidth, availableHeight, columns, rows);
|
||||
}
|
||||
private void action(String action, String itemId) {
|
||||
if (ClientPlayNetworking.canSend(ShopActionPayload.ID)) ClientPlayNetworking.send(new ShopActionPayload(action, itemId));
|
||||
@@ -305,12 +432,14 @@ public final class SanctuaryShopScreen extends Screen {
|
||||
@Override public void onClose() { if (minecraft != null) minecraft.gui.setScreen(parent); }
|
||||
@Override public boolean isPauseScreen() { return false; }
|
||||
|
||||
private record GridLayout(int left, int top, int width, int height) {
|
||||
private record GridLayout(int left, int top, int width, int height, int columns, int rows) {
|
||||
private static final int GAP = 6;
|
||||
Card card(int index) {
|
||||
int cardWidth = (width - GAP * (GRID_COLUMNS - 1)) / GRID_COLUMNS;
|
||||
int cardHeight = (height - GAP * (GRID_ROWS - 1)) / GRID_ROWS;
|
||||
int column = index % GRID_COLUMNS, row = index / GRID_COLUMNS;
|
||||
int cols = Math.max(1, columns);
|
||||
int gridRows = Math.max(1, rows);
|
||||
int cardWidth = (width - GAP * (cols - 1)) / cols;
|
||||
int cardHeight = (height - GAP * (gridRows - 1)) / gridRows;
|
||||
int column = index % cols, row = index / cols;
|
||||
return new Card(left + column * (cardWidth + GAP), top + row * (cardHeight + GAP), cardWidth, cardHeight);
|
||||
}
|
||||
}
|
||||
|
||||
+93
@@ -0,0 +1,93 @@
|
||||
package fr.koka99cab.sanctuary26.sanctuary.shop;
|
||||
|
||||
import java.text.Normalizer;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
import java.util.function.Function;
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
/** Client-side catalogue search, square-grid modes and pagination. */
|
||||
public final class ShopCatalogBrowse {
|
||||
public static final int MIN_GRID = 3;
|
||||
public static final int MAX_GRID = 5;
|
||||
public static final int DEFAULT_GRID = 3;
|
||||
public static final int MAX_SEARCH_LENGTH = 48;
|
||||
private static final Pattern COMBINING_MARKS = Pattern.compile("\\p{M}+");
|
||||
private static final Pattern NON_ALPHANUMERIC = Pattern.compile("[^\\p{L}\\p{N}]+");
|
||||
|
||||
private ShopCatalogBrowse() {
|
||||
}
|
||||
|
||||
public static int clampGrid(int grid) {
|
||||
return Math.max(MIN_GRID, Math.min(MAX_GRID, grid));
|
||||
}
|
||||
|
||||
public static int pageSize(int grid) {
|
||||
int size = clampGrid(grid);
|
||||
return size * size;
|
||||
}
|
||||
|
||||
public static int previousGrid(int grid) {
|
||||
return clampGrid(clampGrid(grid) - 1);
|
||||
}
|
||||
|
||||
public static int nextGrid(int grid) {
|
||||
return clampGrid(clampGrid(grid) + 1);
|
||||
}
|
||||
|
||||
public static int pageCount(int itemCount, int pageSize) {
|
||||
int size = Math.max(0, itemCount);
|
||||
int safePageSize = Math.max(1, pageSize);
|
||||
if (size == 0) return 1;
|
||||
return (size + safePageSize - 1) / safePageSize;
|
||||
}
|
||||
|
||||
public static int clampPage(int page, int itemCount, int pageSize) {
|
||||
return Math.max(0, Math.min(page, pageCount(itemCount, pageSize) - 1));
|
||||
}
|
||||
|
||||
public static String pageLabel(int page, int itemCount, int pageSize) {
|
||||
return (clampPage(page, itemCount, pageSize) + 1) + "/" + pageCount(itemCount, pageSize);
|
||||
}
|
||||
|
||||
public static String normalize(String value) {
|
||||
if (value == null || value.isBlank()) return "";
|
||||
String withoutMarks = COMBINING_MARKS.matcher(
|
||||
Normalizer.normalize(value, Normalizer.Form.NFD)).replaceAll("");
|
||||
return NON_ALPHANUMERIC.matcher(withoutMarks.toLowerCase(Locale.ROOT))
|
||||
.replaceAll(" ").strip();
|
||||
}
|
||||
|
||||
public static boolean matches(String itemId, String displayName, String query) {
|
||||
String normalizedQuery = normalize(query);
|
||||
if (normalizedQuery.isEmpty()) return true;
|
||||
return matches(itemId, displayName, normalizedQuery.split(" "));
|
||||
}
|
||||
|
||||
public static <T> List<T> filter(List<T> items, String query,
|
||||
Function<T, String> itemId, Function<T, String> displayName) {
|
||||
if (items == null || items.isEmpty()) return List.of();
|
||||
String normalizedQuery = normalize(query);
|
||||
if (normalizedQuery.isEmpty()) return List.copyOf(items);
|
||||
String[] terms = normalizedQuery.split(" ");
|
||||
List<T> matches = new ArrayList<>();
|
||||
for (T item : items) {
|
||||
if (matches(itemId.apply(item), displayName.apply(item), terms)) matches.add(item);
|
||||
}
|
||||
return List.copyOf(matches);
|
||||
}
|
||||
|
||||
public static boolean capturesInventoryKey(boolean searchFocused, boolean inventoryKey) {
|
||||
return searchFocused && inventoryKey;
|
||||
}
|
||||
|
||||
private static boolean matches(String itemId, String displayName, String[] terms) {
|
||||
String searchable = normalize((itemId == null ? "" : itemId) + " "
|
||||
+ (displayName == null ? "" : displayName));
|
||||
for (String term : terms) {
|
||||
if (!searchable.contains(term)) return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
}
|
||||
@@ -418,6 +418,11 @@
|
||||
"screen.sanctuary.shop.balance": "%s rubies · %s sapphires",
|
||||
"screen.sanctuary.shop.no_box": "Place your Mailbox nearby to receive parcels.",
|
||||
"screen.sanctuary.shop.catalog_empty": "Your permanent catalogue is empty.",
|
||||
"screen.sanctuary.shop.catalog_no_results": "No catalogue item matches this search.",
|
||||
"screen.sanctuary.shop.search": "Search the catalogue",
|
||||
"screen.sanctuary.shop.search_hint": "Search…",
|
||||
"screen.sanctuary.shop.grid": "%s×%s grid",
|
||||
"screen.sanctuary.shop.page": "%s/%s",
|
||||
"screen.sanctuary.shop.operator": "Operator",
|
||||
"screen.sanctuary.shop.operator_title": "Operator tools",
|
||||
"screen.sanctuary.shop.operator_description": "Server-authoritative shop maintenance.",
|
||||
|
||||
@@ -418,6 +418,11 @@
|
||||
"screen.sanctuary.shop.balance": "%s rubis · %s saphirs",
|
||||
"screen.sanctuary.shop.no_box": "Pose ta boîte aux lettres à proximité pour recevoir les colis.",
|
||||
"screen.sanctuary.shop.catalog_empty": "Ton catalogue permanent est vide.",
|
||||
"screen.sanctuary.shop.catalog_no_results": "Aucun objet du catalogue ne correspond à cette recherche.",
|
||||
"screen.sanctuary.shop.search": "Rechercher dans le catalogue",
|
||||
"screen.sanctuary.shop.search_hint": "Rechercher…",
|
||||
"screen.sanctuary.shop.grid": "Grille %s×%s",
|
||||
"screen.sanctuary.shop.page": "%s/%s",
|
||||
"screen.sanctuary.shop.operator": "Opérateur",
|
||||
"screen.sanctuary.shop.operator_title": "Outils opérateur",
|
||||
"screen.sanctuary.shop.operator_description": "Maintenance du Shop validée par le serveur.",
|
||||
|
||||
@@ -329,6 +329,11 @@
|
||||
"screen.sanctuary.shop.balance": "%s рубинов · %s сапфиров",
|
||||
"screen.sanctuary.shop.no_box": "Поставьте свой ящик доставки рядом, чтобы получать покупки.",
|
||||
"screen.sanctuary.shop.catalog_empty": "Ваш постоянный каталог пуст.",
|
||||
"screen.sanctuary.shop.catalog_no_results": "Ни один предмет каталога не соответствует этому поиску.",
|
||||
"screen.sanctuary.shop.search": "Поиск по каталогу",
|
||||
"screen.sanctuary.shop.search_hint": "Поиск…",
|
||||
"screen.sanctuary.shop.grid": "Сетка %s×%s",
|
||||
"screen.sanctuary.shop.page": "%s/%s",
|
||||
"screen.sanctuary.shop.operator": "Оператор",
|
||||
"screen.sanctuary.shop.operator_title": "Инструменты оператора",
|
||||
"screen.sanctuary.shop.operator_description": "Обслуживание магазина с проверкой на сервере.",
|
||||
|
||||
+109
@@ -0,0 +1,109 @@
|
||||
package fr.koka99cab.sanctuary26.sanctuary.shop;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/** Deterministic coverage for catalogue search, page-size controls and pagination. */
|
||||
public final class ShopCatalogBrowseSmoke {
|
||||
private ShopCatalogBrowseSmoke() {
|
||||
}
|
||||
|
||||
public record Item(String itemId, String displayName) {}
|
||||
|
||||
public static void main(String[] args) {
|
||||
verifySearch();
|
||||
verifyPagination();
|
||||
verifyPageSize();
|
||||
verifyKeyboardRouting();
|
||||
}
|
||||
|
||||
private static void verifySearch() {
|
||||
List<Item> catalog = List.of(
|
||||
new Item("minecraft:diamond", "Diamant"),
|
||||
new Item("minecraft:diamond_sword", "Épée en diamant"),
|
||||
new Item("minecraft:wheat", "Blé"),
|
||||
new Item("anotherworld:ruby", "Rubis"),
|
||||
new Item("minecraft:oak_log", "Bûche de chêne"));
|
||||
require(ShopCatalogBrowse.filter(catalog, "", Item::itemId, Item::displayName).size() == 5,
|
||||
"An empty query must keep the whole catalogue");
|
||||
require(ShopCatalogBrowse.filter(catalog, " ", Item::itemId, Item::displayName).size() == 5,
|
||||
"Whitespace-only search must not hide items");
|
||||
require(ids(catalog, "diam").equals(List.of("minecraft:diamond", "minecraft:diamond_sword")),
|
||||
"Partial names must match both the gem and the sword");
|
||||
require(ids(catalog, "epee").equals(List.of("minecraft:diamond_sword")),
|
||||
"Accent-folded display names must match");
|
||||
require(ids(catalog, "ble").equals(List.of("minecraft:wheat")),
|
||||
"Blé must resolve from ble");
|
||||
require(ids(catalog, "minecraft:wheat").equals(List.of("minecraft:wheat")),
|
||||
"Full item IDs must match");
|
||||
require(ids(catalog, "epee diam").equals(List.of("minecraft:diamond_sword")),
|
||||
"Multi-term search must require every term");
|
||||
require(ids(catalog, "missing").isEmpty(),
|
||||
"Unknown names must produce no results");
|
||||
require(ShopCatalogBrowse.matches("minecraft:diamond", "Diamant", null),
|
||||
"A null query must keep the item visible");
|
||||
require(ShopCatalogBrowse.normalize("Élément spécial").equals("element special"),
|
||||
"Search folding must strip accents and punctuation");
|
||||
}
|
||||
|
||||
private static void verifyPagination() {
|
||||
require(ShopCatalogBrowse.pageCount(0, 9) == 1, "An empty catalogue must keep one page");
|
||||
require(ShopCatalogBrowse.pageCount(9, 9) == 1, "A full default page overflowed");
|
||||
require(ShopCatalogBrowse.pageCount(10, 9) == 2, "The second catalogue page did not open");
|
||||
require(ShopCatalogBrowse.pageCount(20, 16) == 2, "A 4x4 grid must reduce page count");
|
||||
require(ShopCatalogBrowse.pageCount(25, 25) == 1, "A full 5x5 page overflowed");
|
||||
require(ShopCatalogBrowse.clampPage(99, 20, 9) == 2, "A hostile page index was not clamped");
|
||||
require(ShopCatalogBrowse.clampPage(4, 2, 9) == 0,
|
||||
"Search that shrinks the list must snap back to the first page");
|
||||
require(ShopCatalogBrowse.pageLabel(0, 20, 9).equals("1/3"),
|
||||
"The first catalogue page must be labelled 1/3");
|
||||
require(ShopCatalogBrowse.pageLabel(2, 20, 9).equals("3/3"),
|
||||
"The last catalogue page must be labelled 3/3");
|
||||
require(ShopCatalogBrowse.pageLabel(99, 20, 9).equals("3/3"),
|
||||
"An out-of-range page must still show a valid label");
|
||||
require(ShopCatalogBrowse.pageLabel(0, 0, 9).equals("1/1"),
|
||||
"An empty catalogue must keep page 1/1");
|
||||
List<Item> catalog = numbered(20);
|
||||
List<Item> matches = ShopCatalogBrowse.filter(catalog, "item 1", Item::itemId, Item::displayName);
|
||||
require(matches.size() == 11, "Prefix search over numbered labels is incorrect");
|
||||
int page = ShopCatalogBrowse.clampPage(3, matches.size(), 9);
|
||||
require(page == 1, "Filtered pagination must clamp after search");
|
||||
}
|
||||
|
||||
private static void verifyPageSize() {
|
||||
require(ShopCatalogBrowse.clampGrid(0) == 3, "Grid mode must not drop below 3x3");
|
||||
require(ShopCatalogBrowse.clampGrid(9) == 5, "Grid mode must stay at 5x5 maximum");
|
||||
require(ShopCatalogBrowse.pageSize(3) == 9, "3x3 must show nine items");
|
||||
require(ShopCatalogBrowse.pageSize(4) == 16, "4x4 must show sixteen items");
|
||||
require(ShopCatalogBrowse.pageSize(5) == 25, "5x5 must show twenty-five items");
|
||||
require(ShopCatalogBrowse.nextGrid(3) == 4, "The next mode after 3x3 must be 4x4");
|
||||
require(ShopCatalogBrowse.nextGrid(5) == 5, "5x5 must be the last catalogue grid");
|
||||
require(ShopCatalogBrowse.previousGrid(4) == 3, "The previous mode after 4x4 must be 3x3");
|
||||
require(ShopCatalogBrowse.previousGrid(3) == 3, "3x3 must not shrink further");
|
||||
}
|
||||
|
||||
private static void verifyKeyboardRouting() {
|
||||
require(ShopCatalogBrowse.capturesInventoryKey(true, true),
|
||||
"A focused search field must keep the inventory key");
|
||||
require(!ShopCatalogBrowse.capturesInventoryKey(false, true),
|
||||
"An unfocused catalogue must still close on the inventory key");
|
||||
require(!ShopCatalogBrowse.capturesInventoryKey(true, false),
|
||||
"Unrelated keys must not be swallowed by catalogue search");
|
||||
}
|
||||
|
||||
private static List<String> ids(List<Item> catalog, String query) {
|
||||
return ShopCatalogBrowse.filter(catalog, query, Item::itemId, Item::displayName)
|
||||
.stream().map(Item::itemId).toList();
|
||||
}
|
||||
|
||||
private static List<Item> numbered(int count) {
|
||||
List<Item> items = new java.util.ArrayList<>();
|
||||
for (int index = 1; index <= count; index++) {
|
||||
items.add(new Item("minecraft:item_" + index, "Item " + index));
|
||||
}
|
||||
return List.copyOf(items);
|
||||
}
|
||||
|
||||
private static void require(boolean value, String message) {
|
||||
if (!value) throw new AssertionError(message);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user