diff --git a/NOTICE b/NOTICE index f0c5c7d..7b6b23f 100644 --- a/NOTICE +++ b/NOTICE @@ -8,6 +8,8 @@ Mystcraft Legacy copyright: XCompWiz and contributors Mystcraft Legacy license: GNU Lesser General Public License version 3 The animated ink-vial item texture in this project is copied from Mystcraft Legacy's assets/mystcraft/textures/items/ink_vial.png under that license. +The Notebook page atlas is copied from Legacy's assets/mystcraft/gui/bookui_pagel.png +under the same license. The initial build layout is derived from the official NeoForge 1.21.1 ModDevGradle MDK: https://github.com/NeoForgeMDKs/MDK-1.21.1-ModDevGradle diff --git a/docs/ASSET_PROVENANCE.md b/docs/ASSET_PROVENANCE.md index 70bca3a..c68d464 100644 --- a/docs/ASSET_PROVENANCE.md +++ b/docs/ASSET_PROVENANCE.md @@ -7,6 +7,7 @@ project's GNU LGPL v3 license. Rivencraft itself uses the same license. | Rivencraft asset | Upstream asset | Upstream revision | Changes | | --- | --- | --- | --- | | `assets/rivencraft/textures/item/page.png` | `assets/mystcraft/textures/items/page_background.png` | `9bc8ddc061845df0cd5ea47f4fdef773cff786ae` | Namespace/path rename only | +| `assets/rivencraft/textures/gui/notebook_pages.png` | `assets/mystcraft/gui/bookui_pagel.png` | `9bc8ddc061845df0cd5ea47f4fdef773cff786ae` | Namespace/path rename only; Notebook draws the native 30×40 page at atlas coordinates `(156, 0)` | | `assets/rivencraft/textures/item/linking_book_unwritten.png` | `assets/mystcraft/textures/items/unlinked.png` | `9bc8ddc061845df0cd5ea47f4fdef773cff786ae` | Namespace/path rename only | | `assets/rivencraft/textures/item/page_link_panel.png` | `page_background.png` and `PageBuilder.BasicPageSprite` | `9bc8ddc061845df0cd5ea47f4fdef773cff786ae` | Reproduces the legacy 160 px scale, black panel at `(25,30)`–`(135,75)`, and 128 px resample | | `assets/rivencraft/textures/item/page_symbol*.png` and `page_unresolved.png` | `page_background.png`, `textures/symbolcomponents.png`, `WordData`, `ModSymbols`, and `PageBuilder.PageSprite` | `9bc8ddc061845df0cd5ea47f4fdef773cff786ae` | Static 1.21.1 renditions of the legacy runtime-composed four-word symbol pages; `normal_time` uses the legacy Full Length poem | diff --git a/docs/WRITING_DATA.md b/docs/WRITING_DATA.md index 883627a..fc6a25a 100644 --- a/docs/WRITING_DATA.md +++ b/docs/WRITING_DATA.md @@ -65,6 +65,13 @@ composition from its page background and symbol-component sheet. Data-pack symbols without a bundled resource-pack model use the legacy question-mark fallback while keeping their real identifier and display metadata. +The Notebook surface displays pages by their persistent slot index instead of +packing occupied pages together. Empty positions through the last placed page +appear darkened. While carrying a Page, all available positions are shown for +placement. The surface draws the native 30×40 portrait page region from +Legacy's book UI atlas; symbols are layered at a uniform scale, while the +square item models remain unchanged in inventories. + ## Ink and baseline link panels An Ink Vial is crafted from two black dye, a glass bottle, and a water bucket. diff --git a/gradle.properties b/gradle.properties index e7ac29e..e5133a9 100644 --- a/gradle.properties +++ b/gradle.properties @@ -35,7 +35,7 @@ mod_name=Rivencraft # The license of the mod. Review your options at https://choosealicense.com/. All Rights Reserved is the default. mod_license=LGPL-3.0-or-later # The mod version. Rivencraft uses the x.y.z.b policy documented in README.md. -mod_version=0.3.4.1 +mod_version=0.3.4.2 immersive_portals_version=6.0.7 # The group ID for the mod. It is only important when publishing as an artifact to a Maven repository. # This should match the base package used for the mod sources. diff --git a/src/main/java/net/dnorberg/rivencraft/client/screen/NotebookScreen.java b/src/main/java/net/dnorberg/rivencraft/client/screen/NotebookScreen.java index 50ced28..81ebcbb 100644 --- a/src/main/java/net/dnorberg/rivencraft/client/screen/NotebookScreen.java +++ b/src/main/java/net/dnorberg/rivencraft/client/screen/NotebookScreen.java @@ -1,8 +1,8 @@ package net.dnorberg.rivencraft.client.screen; -import java.util.ArrayList; -import java.util.List; import java.util.Locale; +import net.dnorberg.rivencraft.Rivencraft; +import net.dnorberg.rivencraft.item.PageItem; import net.dnorberg.rivencraft.menu.NotebookMenu; import net.dnorberg.rivencraft.registry.ModItems; import net.minecraft.client.gui.GuiGraphics; @@ -10,6 +10,7 @@ import net.minecraft.client.gui.components.Button; import net.minecraft.client.gui.components.EditBox; import net.minecraft.client.gui.screens.inventory.AbstractContainerScreen; import net.minecraft.network.chat.Component; +import net.minecraft.resources.ResourceLocation; import net.minecraft.world.entity.player.Inventory; import net.minecraft.world.inventory.ClickType; import net.minecraft.world.item.ItemStack; @@ -24,6 +25,15 @@ public final class NotebookScreen extends AbstractContainerScreen private static final int PAGE_STEP_X = 31; private static final int PAGE_STEP_Y = 41; private static final int PAGE_COLUMNS = 5; + private static final ResourceLocation PAGE_ATLAS = texture("gui/notebook_pages"); + private static final ResourceLocation GENERIC_SYMBOL = texture("item/page_symbol"); + private static final ResourceLocation UNRESOLVED_SYMBOL = texture("item/page_unresolved"); + private static final ResourceLocation STANDARD_TERRAIN = texture("item/page_symbol_terrain_standard"); + private static final ResourceLocation NATIVE_BIOMES = texture("item/page_symbol_biome_native"); + private static final ResourceLocation NORMAL_LIGHTING = texture("item/page_symbol_lighting_normal"); + private static final ResourceLocation NORMAL_WEATHER = texture("item/page_symbol_weather_normal"); + private static final ResourceLocation NORMAL_TIME = texture("item/page_symbol_celestial_normal_time"); + private static final ResourceLocation CAVES = texture("item/page_symbol_feature_caves"); private EditBox textInput; private boolean draggingScrollbar; private boolean surfaceClickPendingRelease; @@ -59,25 +69,25 @@ public final class NotebookScreen extends AbstractContainerScreen @Override public void render(GuiGraphics graphics, int mouseX, int mouseY, float partialTick) { super.render(graphics, mouseX, mouseY, partialTick); - List pages = visiblePages(); - scroll = Math.min(scroll, maxScroll(pages.size())); + int pageSlots = displayedPageSlots(); + scroll = Math.min(scroll, maxScroll(pageSlots)); graphics.enableScissor(leftPos, topPos + SURFACE_TOP, leftPos + SURFACE_RIGHT, topPos + SURFACE_BOTTOM); - for (int position = 0; position < pages.size(); position++) { - int x = leftPos + (position % PAGE_COLUMNS) * PAGE_STEP_X; - int y = topPos + SURFACE_TOP + (position / PAGE_COLUMNS) * PAGE_STEP_Y - scroll; + for (int index = 0; index < pageSlots; index++) { + int x = leftPos + (index % PAGE_COLUMNS) * PAGE_STEP_X; + int y = topPos + SURFACE_TOP + (index / PAGE_COLUMNS) * PAGE_STEP_Y - scroll; if (y + PAGE_HEIGHT > topPos + SURFACE_TOP && y < topPos + SURFACE_BOTTOM) { - drawPage(graphics, pages.get(position), x, y); + drawPage(graphics, index, x, y); } } graphics.disableScissor(); - drawScrollbar(graphics, pages.size()); - int hovered = pageAt(mouseX, mouseY, pages); + drawScrollbar(graphics, pageSlots); + int hovered = pageAt(mouseX, mouseY); if (!menu.getCarried().isEmpty() && inSurface(mouseX, mouseY)) { graphics.renderItem(menu.getCarried(), mouseX - 8, mouseY - 8); - } else if (hovered >= 0) { + } else if (hovered >= 0 && !menu.getSlot(hovered).getItem().isEmpty()) { ItemStack page = menu.getSlot(hovered).getItem(); - if (!page.isEmpty() && matchesSearch(page)) graphics.renderTooltip(font, page, mouseX, mouseY); + if (matchesSearch(page)) graphics.renderTooltip(font, page, mouseX, mouseY); } else { renderTooltip(graphics, mouseX, mouseY); } @@ -103,13 +113,16 @@ public final class NotebookScreen extends AbstractContainerScreen public boolean mouseClicked(double mouseX, double mouseY, int button) { if (inScrollbar(mouseX, mouseY) && button == 0) { draggingScrollbar = true; - setScrollFromMouse(mouseY, visiblePages().size()); + setScrollFromMouse(mouseY, displayedPageSlots()); return true; } - int index = pageAt(mouseX, mouseY, visiblePages()); + int index = pageAt(mouseX, mouseY); if (index >= 0 && (button == 0 || button == 1)) { ItemStack page = menu.getSlot(index).getItem(); - if (!page.isEmpty() && !matchesSearch(page)) return true; + if ((!page.isEmpty() && !matchesSearch(page)) + || (page.isEmpty() && !menu.getCarried().is(ModItems.PAGE.get()))) { + return true; + } setFocused(null); textInput.setFocused(false); selectedSlot = index; @@ -123,7 +136,7 @@ public final class NotebookScreen extends AbstractContainerScreen @Override public boolean mouseDragged(double mouseX, double mouseY, int button, double dragX, double dragY) { if (draggingScrollbar && button == 0) { - setScrollFromMouse(mouseY, visiblePages().size()); + setScrollFromMouse(mouseY, displayedPageSlots()); return true; } return super.mouseDragged(mouseX, mouseY, button, dragX, dragY); @@ -144,7 +157,7 @@ public final class NotebookScreen extends AbstractContainerScreen public boolean mouseScrolled(double mouseX, double mouseY, double scrollX, double scrollY) { if (inSurface(mouseX, mouseY) || inScrollbar(mouseX, mouseY)) { scroll = Math.clamp(scroll - (int)Math.signum(scrollY) * 20, - 0, maxScroll(visiblePages().size())); + 0, maxScroll(displayedPageSlots())); return true; } return super.mouseScrolled(mouseX, mouseY, scrollX, scrollY); @@ -170,21 +183,16 @@ public final class NotebookScreen extends AbstractContainerScreen return super.keyPressed(keyCode, scanCode, modifiers); } - private List visiblePages() { - List pages = new ArrayList<>(); - int firstEmpty = -1; - for (int index = 0; index < NotebookMenu.NOTEBOOK_SLOTS; index++) { - ItemStack stack = menu.getSlot(index).getItem(); - if (stack.isEmpty()) { - if (firstEmpty < 0) firstEmpty = index; - } else { - pages.add(index); + private int displayedPageSlots() { + if (menu.getCarried().is(ModItems.PAGE.get())) { + return NotebookMenu.NOTEBOOK_SLOTS; + } + for (int index = NotebookMenu.NOTEBOOK_SLOTS - 1; index >= 0; index--) { + if (!menu.getSlot(index).getItem().isEmpty()) { + return index + 1; } } - if (firstEmpty >= 0 && menu.getCarried().is(ModItems.PAGE.get())) { - pages.add(firstEmpty); - } - return pages; + return 0; } private boolean matchesSearch(ItemStack page) { @@ -193,15 +201,15 @@ public final class NotebookScreen extends AbstractContainerScreen .contains(textInput.getValue().toLowerCase(Locale.ROOT)); } - private int pageAt(double mouseX, double mouseY, List pages) { + private int pageAt(double mouseX, double mouseY) { if (!inSurface(mouseX, mouseY)) return -1; int localX = (int)mouseX - leftPos; int localY = (int)mouseY - topPos - SURFACE_TOP + scroll; int column = localX / PAGE_STEP_X; - int position = (localY / PAGE_STEP_Y) * PAGE_COLUMNS + column; + int index = (localY / PAGE_STEP_Y) * PAGE_COLUMNS + column; return column < PAGE_COLUMNS && localX % PAGE_STEP_X < PAGE_WIDTH - && localY % PAGE_STEP_Y < PAGE_HEIGHT && position < pages.size() - ? pages.get(position) : -1; + && localY % PAGE_STEP_Y < PAGE_HEIGHT && index < displayedPageSlots() + ? index : -1; } private boolean inSurface(double mouseX, double mouseY) { @@ -243,20 +251,44 @@ public final class NotebookScreen extends AbstractContainerScreen private void drawPage(GuiGraphics graphics, int index, int x, int y) { ItemStack page = menu.getSlot(index).getItem(); - boolean visible = !page.isEmpty() && matchesSearch(page); - graphics.fill(x, y, x + PAGE_WIDTH, y + PAGE_HEIGHT, - visible ? 0xFFE5D4A7 : 0x704B4945); - graphics.fill(x + 2, y + 2, x + PAGE_WIDTH - 2, y + PAGE_HEIGHT - 2, - visible ? 0xFFF4E5C3 : 0x603B3934); - graphics.renderOutline(x, y, PAGE_WIDTH, PAGE_HEIGHT, - index == selectedSlot ? 0xFFFFD061 : 0xFF564A34); - if (visible) { - graphics.pose().pushPose(); - graphics.pose().translate(x + 3, y + 8, 0); - graphics.pose().scale(1.5F, 1.5F, 1.0F); - graphics.renderItem(page, 0, 0); - graphics.pose().popPose(); + boolean dimmed = page.isEmpty() || !matchesSearch(page); + // Legacy's Notebook draws the atlas' native 30x40 page, not a stretched item icon. + graphics.blit(PAGE_ATLAS, x, y, PAGE_WIDTH, PAGE_HEIGHT, + 156, 0, PAGE_WIDTH, PAGE_HEIGHT, 256, 256); + if (!page.isEmpty()) { + switch (PageItem.content(page).kind()) { + case BLANK -> { } + case LINK_PANEL -> graphics.fill(x + 5, y + 6, x + 25, y + 20, 0xFF000000); + case SYMBOL, UNRESOLVED_SYMBOL -> graphics.blit(symbolTexture(page), x + 1, y + 6, + 28, 28, 16, 16, 96, 96, 128, 128); + } } + if (dimmed) { + graphics.fill(x, y, x + PAGE_WIDTH, y + PAGE_HEIGHT, 0xB8000000); + } + if (index == selectedSlot && !page.isEmpty()) { + graphics.renderOutline(x, y, PAGE_WIDTH, PAGE_HEIGHT, 0xFFFFD061); + } + } + + private static ResourceLocation symbolTexture(ItemStack page) { + var content = PageItem.content(page); + if (content.kind() == net.dnorberg.rivencraft.writing.PageContent.Kind.UNRESOLVED_SYMBOL) { + return UNRESOLVED_SYMBOL; + } + return switch (content.symbol().orElseThrow().id().toString()) { + case "rivencraft:terrain/standard" -> STANDARD_TERRAIN; + case "rivencraft:biome/native" -> NATIVE_BIOMES; + case "rivencraft:lighting/normal" -> NORMAL_LIGHTING; + case "rivencraft:weather/normal" -> NORMAL_WEATHER; + case "rivencraft:celestial/normal_time" -> NORMAL_TIME; + case "rivencraft:feature/caves" -> CAVES; + default -> GENERIC_SYMBOL; + }; + } + + private static ResourceLocation texture(String path) { + return ResourceLocation.fromNamespaceAndPath(Rivencraft.MOD_ID, "textures/" + path + ".png"); } private void moveSelected(boolean next) { diff --git a/src/main/java/net/dnorberg/rivencraft/gametest/WritingDataGameTests.java b/src/main/java/net/dnorberg/rivencraft/gametest/WritingDataGameTests.java index 9b36557..bcff5db 100644 --- a/src/main/java/net/dnorberg/rivencraft/gametest/WritingDataGameTests.java +++ b/src/main/java/net/dnorberg/rivencraft/gametest/WritingDataGameTests.java @@ -203,6 +203,29 @@ public final class WritingDataGameTests { helper.succeed(); } + @GameTest(template = "empty") + public static void notebookMenuPreservesSparsePagePositions(GameTestHelper helper) { + var player = helper.makeMockServerPlayerInLevel(); + player.setItemInHand(InteractionHand.MAIN_HAND, new ItemStack(ModItems.NOTEBOOK.get())); + NotebookMenu menu = new NotebookMenu(9, player.getInventory(), InteractionHand.MAIN_HAND); + ItemStack page = PageItem.createSymbolPage(reference("rivencraft:terrain/standard")); + menu.setCarried(page); + + menu.clicked(9, 0, ClickType.PICKUP, player); + helper.assertTrue(menu.getCarried().isEmpty(), + "Clicking a distant empty Notebook slot must place the carried Page"); + NotebookContents saved = player.getMainHandItem().get(ModDataComponents.NOTEBOOK_CONTENTS.get()); + helper.assertTrue(saved != null && saved.pages().size() > 9, + "Notebook persistence must extend through the highest occupied position"); + for (int index = 0; index < 9; index++) { + helper.assertTrue(saved.pages().get(index).isEmpty(), + "Earlier empty page positions must remain empty rather than being packed together"); + } + helper.assertValueEqual(PageItem.content(saved.pages().get(9)).kind(), PageContent.Kind.SYMBOL, + "The Page must remain in the selected distant position"); + helper.succeed(); + } + @GameTest(template = "empty") public static void inkVialsPersistAndOnlyStackWithMatchingContents(GameTestHelper helper) { ItemStack vial = new ItemStack(ModItems.INK_VIAL.get()); diff --git a/src/main/resources/assets/rivencraft/textures/gui/notebook_pages.png b/src/main/resources/assets/rivencraft/textures/gui/notebook_pages.png new file mode 100644 index 0000000..6bc7d4f Binary files /dev/null and b/src/main/resources/assets/rivencraft/textures/gui/notebook_pages.png differ