Public Access
This commit is contained in:
1 parent
143da0a940
commit
d6fd32994f
16 files changed
+298
-3
No files matched your search
@@ -15,6 +15,8 @@ public final class RivencraftConfig {
|
||||
|
||||
public static final ModConfigSpec SPEC;
|
||||
private static final ModConfigSpec.EnumValue<ReturnLinkMode> RETURN_LINK_MODE;
|
||||
private static final ModConfigSpec.IntValue SYMBOL_INK_COST;
|
||||
private static final ModConfigSpec.IntValue LINK_PANEL_INK_COST;
|
||||
|
||||
static {
|
||||
ModConfigSpec.Builder builder = new ModConfigSpec.Builder();
|
||||
@@ -25,6 +27,12 @@ public final class RivencraftConfig {
|
||||
)
|
||||
.defineEnum("return_link_mode", ReturnLinkMode.ON_CREATION);
|
||||
builder.pop();
|
||||
builder.push("writing");
|
||||
SYMBOL_INK_COST = builder.comment("Ink units consumed when the Writing Desk copies one symbol page.")
|
||||
.defineInRange("symbol_ink_cost", 1, 1, 16);
|
||||
LINK_PANEL_INK_COST = builder.comment("Ink units consumed when preparing one baseline link-panel page.")
|
||||
.defineInRange("link_panel_ink_cost", 2, 1, 16);
|
||||
builder.pop();
|
||||
SPEC = builder.build();
|
||||
}
|
||||
|
||||
@@ -34,4 +42,12 @@ public final class RivencraftConfig {
|
||||
public static ReturnLinkMode returnLinkMode() {
|
||||
return RETURN_LINK_MODE.get();
|
||||
}
|
||||
|
||||
public static int symbolInkCost() {
|
||||
return SYMBOL_INK_COST.get();
|
||||
}
|
||||
|
||||
public static int linkPanelInkCost() {
|
||||
return LINK_PANEL_INK_COST.get();
|
||||
}
|
||||
}
|
||||
@@ -11,6 +11,8 @@ import net.dnorberg.rivencraft.writing.SymbolReference;
|
||||
import net.dnorberg.rivencraft.writing.WritingEntry;
|
||||
import net.dnorberg.rivencraft.writing.NotebookContents;
|
||||
import net.dnorberg.rivencraft.item.PageItem;
|
||||
import net.dnorberg.rivencraft.item.InkVialItem;
|
||||
import net.dnorberg.rivencraft.writing.WritingInkService;
|
||||
import net.dnorberg.rivencraft.registry.ModDataComponents;
|
||||
import net.dnorberg.rivencraft.registry.ModItems;
|
||||
import net.dnorberg.rivencraft.menu.NotebookMenu;
|
||||
@@ -200,6 +202,95 @@ public final class WritingDataGameTests {
|
||||
helper.succeed();
|
||||
}
|
||||
|
||||
@GameTest(template = "empty")
|
||||
public static void inkVialsPersistAndOnlyStackWithMatchingContents(GameTestHelper helper) {
|
||||
ItemStack vial = new ItemStack(ModItems.INK_VIAL.get());
|
||||
vial.set(ModDataComponents.INK_REMAINING.get(), 7);
|
||||
CompoundTag serialized = (CompoundTag) vial.saveOptional(helper.getLevel().registryAccess());
|
||||
ItemStack restored = ItemStack.parseOptional(helper.getLevel().registryAccess(), serialized);
|
||||
helper.assertValueEqual(InkVialItem.remaining(restored), 7,
|
||||
"Remaining ink must survive ItemStack persistence");
|
||||
helper.assertTrue(ItemStack.isSameItemSameComponents(vial, restored),
|
||||
"Vials with equal ink contents must be eligible to stack");
|
||||
helper.assertFalse(ItemStack.isSameItemSameComponents(vial, new ItemStack(ModItems.INK_VIAL.get())),
|
||||
"Vials with different ink contents must not stack");
|
||||
helper.succeed();
|
||||
}
|
||||
|
||||
@GameTest(template = "empty")
|
||||
public static void linkPanelPreparationConsumesOnePageAndConfiguredInkOnlyOnSuccess(GameTestHelper helper) {
|
||||
var player = helper.makeMockServerPlayerInLevel();
|
||||
ItemStack pages = new ItemStack(ModItems.PAGE.get(), 2);
|
||||
ItemStack vial = new ItemStack(ModItems.INK_VIAL.get());
|
||||
player.setItemInHand(InteractionHand.MAIN_HAND, vial);
|
||||
player.setItemInHand(InteractionHand.OFF_HAND, pages);
|
||||
|
||||
helper.assertTrue(WritingInkService.prepareLinkPanel(player, InteractionHand.MAIN_HAND),
|
||||
"A blank Page and sufficient ink must prepare one link panel");
|
||||
helper.assertValueEqual(pages.getCount(), 1, "Preparation must consume exactly one blank Page");
|
||||
helper.assertValueEqual(InkVialItem.remaining(player.getMainHandItem()),
|
||||
InkVialItem.CAPACITY - WritingInkService.linkPanelCost(),
|
||||
"Preparation must consume exactly the server-configured ink cost");
|
||||
long panels = player.getInventory().items.stream()
|
||||
.filter(stack -> stack.is(ModItems.PAGE.get())
|
||||
&& PageItem.content(stack).kind() == PageContent.Kind.LINK_PANEL)
|
||||
.mapToInt(ItemStack::getCount).sum();
|
||||
helper.assertValueEqual(panels, 1L, "Preparation must output exactly one link panel");
|
||||
|
||||
ItemStack written = PageItem.createSymbolPage(reference("rivencraft:terrain/standard"));
|
||||
player.setItemInHand(InteractionHand.OFF_HAND, written);
|
||||
int inkBefore = InkVialItem.remaining(player.getMainHandItem());
|
||||
helper.assertFalse(WritingInkService.prepareLinkPanel(player, InteractionHand.MAIN_HAND),
|
||||
"A written Page must not be overwritten as a link panel");
|
||||
helper.assertValueEqual(InkVialItem.remaining(player.getMainHandItem()), inkBefore,
|
||||
"Rejected writing must not consume ink");
|
||||
helper.assertValueEqual(PageItem.content(written).kind(), PageContent.Kind.SYMBOL,
|
||||
"Rejected writing must not modify the input Page");
|
||||
|
||||
ItemStack lowInk = new ItemStack(ModItems.INK_VIAL.get());
|
||||
lowInk.set(ModDataComponents.INK_REMAINING.get(), WritingInkService.linkPanelCost() - 1);
|
||||
player.setItemInHand(InteractionHand.MAIN_HAND, lowInk);
|
||||
player.setItemInHand(InteractionHand.OFF_HAND, new ItemStack(ModItems.PAGE.get()));
|
||||
helper.assertFalse(WritingInkService.prepareLinkPanel(player, InteractionHand.MAIN_HAND),
|
||||
"Insufficient ink must reject link-panel preparation");
|
||||
helper.assertValueEqual(InkVialItem.remaining(player.getMainHandItem()),
|
||||
WritingInkService.linkPanelCost() - 1,
|
||||
"Insufficient ink must remain untouched");
|
||||
helper.assertValueEqual(PageItem.content(player.getOffhandItem()).kind(), PageContent.Kind.BLANK,
|
||||
"Insufficient ink must leave the Page blank");
|
||||
|
||||
lowInk.set(ModDataComponents.INK_REMAINING.get(), WritingInkService.linkPanelCost());
|
||||
helper.assertTrue(WritingInkService.prepareLinkPanel(player, InteractionHand.MAIN_HAND),
|
||||
"Exact remaining ink must be usable");
|
||||
helper.assertTrue(player.getMainHandItem().is(Items.GLASS_BOTTLE),
|
||||
"A depleted Ink Vial must return a reusable glass bottle");
|
||||
helper.assertValueEqual(PageItem.content(player.getOffhandItem()).kind(),
|
||||
PageContent.Kind.LINK_PANEL,
|
||||
"A single blank Page must be replaced by one Link Panel Page");
|
||||
helper.succeed();
|
||||
}
|
||||
|
||||
@GameTest(template = "empty")
|
||||
public static void stackedInkVialsSplitWithoutChangingTheOtherVials(GameTestHelper helper) {
|
||||
var player = helper.makeMockServerPlayerInLevel();
|
||||
player.setItemInHand(InteractionHand.MAIN_HAND, new ItemStack(ModItems.INK_VIAL.get(), 2));
|
||||
player.setItemInHand(InteractionHand.OFF_HAND, new ItemStack(ModItems.PAGE.get()));
|
||||
|
||||
helper.assertTrue(WritingInkService.prepareLinkPanel(player, InteractionHand.MAIN_HAND),
|
||||
"A stack of full vials must support preparing one panel");
|
||||
helper.assertValueEqual(player.getMainHandItem().getCount(), 1,
|
||||
"Using one vial must leave the other full vial in hand");
|
||||
helper.assertValueEqual(InkVialItem.remaining(player.getMainHandItem()), InkVialItem.CAPACITY,
|
||||
"The unconsumed vial must remain full");
|
||||
long partialVials = player.getInventory().items.stream()
|
||||
.filter(stack -> stack.is(ModItems.INK_VIAL.get())
|
||||
&& InkVialItem.remaining(stack) == InkVialItem.CAPACITY - WritingInkService.linkPanelCost())
|
||||
.mapToInt(ItemStack::getCount).sum();
|
||||
helper.assertValueEqual(partialVials, 1L,
|
||||
"The used vial must reappear with only its own ink reduced");
|
||||
helper.succeed();
|
||||
}
|
||||
|
||||
private static SymbolReference reference(String id) {
|
||||
return new SymbolReference(ResourceLocation.parse(id));
|
||||
}
|
||||
|
||||
@@ -0,0 +1,47 @@
|
||||
package net.dnorberg.rivencraft.item;
|
||||
|
||||
import java.util.List;
|
||||
import net.dnorberg.rivencraft.registry.ModDataComponents;
|
||||
import net.dnorberg.rivencraft.writing.WritingInkService;
|
||||
import net.minecraft.ChatFormatting;
|
||||
import net.minecraft.network.chat.Component;
|
||||
import net.minecraft.world.InteractionHand;
|
||||
import net.minecraft.world.InteractionResultHolder;
|
||||
import net.minecraft.world.entity.player.Player;
|
||||
import net.minecraft.world.item.Item;
|
||||
import net.minecraft.world.item.ItemStack;
|
||||
import net.minecraft.world.item.TooltipFlag;
|
||||
import net.minecraft.world.level.Level;
|
||||
|
||||
/** A stackable vial; the ink component is per vial, not shared across a stack. */
|
||||
public final class InkVialItem extends Item {
|
||||
public static final int CAPACITY = 16;
|
||||
|
||||
public InkVialItem(Properties properties) {
|
||||
super(properties);
|
||||
}
|
||||
|
||||
public static int remaining(ItemStack vial) {
|
||||
return vial.getOrDefault(ModDataComponents.INK_REMAINING.get(), CAPACITY);
|
||||
}
|
||||
|
||||
@Override
|
||||
public InteractionResultHolder<ItemStack> use(Level level, Player player, InteractionHand hand) {
|
||||
if (!WritingInkService.canPrepareLinkPanel(player, hand)) {
|
||||
return InteractionResultHolder.pass(player.getItemInHand(hand));
|
||||
}
|
||||
if (!level.isClientSide) {
|
||||
WritingInkService.prepareLinkPanel(player, hand);
|
||||
}
|
||||
return InteractionResultHolder.sidedSuccess(player.getItemInHand(hand), level.isClientSide);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void appendHoverText(ItemStack stack, TooltipContext context,
|
||||
List<Component> tooltip, TooltipFlag flag) {
|
||||
tooltip.add(Component.translatable("item.rivencraft.ink_vial.remaining",
|
||||
remaining(stack), CAPACITY).withStyle(ChatFormatting.GRAY));
|
||||
tooltip.add(Component.translatable("item.rivencraft.ink_vial.usage")
|
||||
.withStyle(ChatFormatting.GRAY));
|
||||
}
|
||||
}
|
||||
@@ -21,6 +21,7 @@ public final class ModCreativeTabs {
|
||||
output.accept(ModItems.LINKING_BOOK.get());
|
||||
output.accept(ModItems.PAGE.get());
|
||||
output.accept(ModItems.NOTEBOOK.get());
|
||||
output.accept(ModItems.INK_VIAL.get());
|
||||
})
|
||||
.build());
|
||||
|
||||
|
||||
@@ -6,6 +6,7 @@ import net.dnorberg.rivencraft.book.AgeReference;
|
||||
import net.dnorberg.rivencraft.writing.PageContent;
|
||||
import net.dnorberg.rivencraft.writing.NotebookContents;
|
||||
import net.dnorberg.rivencraft.writing.SymbolDisplayMetadata;
|
||||
import com.mojang.serialization.Codec;
|
||||
import net.minecraft.core.component.DataComponentType;
|
||||
import net.minecraft.core.registries.Registries;
|
||||
import net.minecraft.resources.ResourceLocation;
|
||||
@@ -60,6 +61,12 @@ public final class ModDataComponents {
|
||||
.networkSynchronized(NotebookContents.STREAM_CODEC)
|
||||
.cacheEncoding()
|
||||
.build());
|
||||
public static final DeferredHolder<DataComponentType<?>, DataComponentType<Integer>> INK_REMAINING =
|
||||
DATA_COMPONENTS.register("ink_remaining", () -> DataComponentType.<Integer>builder()
|
||||
.persistent(Codec.intRange(0, 16))
|
||||
.networkSynchronized(ByteBufCodecs.VAR_INT)
|
||||
.cacheEncoding()
|
||||
.build());
|
||||
|
||||
private ModDataComponents() {
|
||||
}
|
||||
|
||||
@@ -5,6 +5,7 @@ import net.dnorberg.rivencraft.item.DescriptiveBookItem;
|
||||
import net.dnorberg.rivencraft.item.LinkingBookItem;
|
||||
import net.dnorberg.rivencraft.item.PageItem;
|
||||
import net.dnorberg.rivencraft.item.NotebookItem;
|
||||
import net.dnorberg.rivencraft.item.InkVialItem;
|
||||
import net.minecraft.world.item.Item;
|
||||
import net.neoforged.bus.api.IEventBus;
|
||||
import net.neoforged.neoforge.registries.DeferredItem;
|
||||
@@ -33,6 +34,11 @@ public final class ModItems {
|
||||
NotebookItem::new,
|
||||
new Item.Properties().stacksTo(1)
|
||||
);
|
||||
public static final DeferredItem<InkVialItem> INK_VIAL = ITEMS.registerItem(
|
||||
"ink_vial",
|
||||
InkVialItem::new,
|
||||
new Item.Properties().stacksTo(16)
|
||||
);
|
||||
|
||||
private ModItems() {
|
||||
}
|
||||
|
||||
@@ -0,0 +1,77 @@
|
||||
package net.dnorberg.rivencraft.writing;
|
||||
|
||||
import net.dnorberg.rivencraft.config.RivencraftConfig;
|
||||
import net.dnorberg.rivencraft.item.InkVialItem;
|
||||
import net.dnorberg.rivencraft.item.PageItem;
|
||||
import net.dnorberg.rivencraft.registry.ModDataComponents;
|
||||
import net.dnorberg.rivencraft.registry.ModItems;
|
||||
import net.minecraft.world.InteractionHand;
|
||||
import net.minecraft.world.entity.player.Player;
|
||||
import net.minecraft.world.item.ItemStack;
|
||||
import net.minecraft.world.item.Items;
|
||||
|
||||
/** Server-owned costs and the baseline blank-page-to-link-panel transaction. */
|
||||
public final class WritingInkService {
|
||||
private WritingInkService() {
|
||||
}
|
||||
|
||||
public static int symbolCopyCost() {
|
||||
return RivencraftConfig.symbolInkCost();
|
||||
}
|
||||
|
||||
public static int linkPanelCost() {
|
||||
return RivencraftConfig.linkPanelInkCost();
|
||||
}
|
||||
|
||||
public static boolean canPrepareLinkPanel(Player player, InteractionHand vialHand) {
|
||||
ItemStack vial = player.getItemInHand(vialHand);
|
||||
ItemStack page = player.getItemInHand(other(vialHand));
|
||||
int remaining = InkVialItem.remaining(vial);
|
||||
return vial.is(ModItems.INK_VIAL.get()) && remaining >= linkPanelCost()
|
||||
&& remaining <= InkVialItem.CAPACITY
|
||||
&& page.is(ModItems.PAGE.get()) && PageItem.content(page).kind() == PageContent.Kind.BLANK;
|
||||
}
|
||||
|
||||
public static boolean prepareLinkPanel(Player player, InteractionHand vialHand) {
|
||||
if (player.level().isClientSide || !canPrepareLinkPanel(player, vialHand)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
InteractionHand pageHand = other(vialHand);
|
||||
ItemStack page = player.getItemInHand(pageHand);
|
||||
ItemStack vial = player.getItemInHand(vialHand);
|
||||
int newInk = InkVialItem.remaining(vial) - linkPanelCost();
|
||||
ItemStack panel = PageItem.createLinkPanelPage();
|
||||
ItemStack spentVial = newInk == 0
|
||||
? new ItemStack(Items.GLASS_BOTTLE)
|
||||
: new ItemStack(ModItems.INK_VIAL.get());
|
||||
if (newInk > 0) {
|
||||
spentVial.set(ModDataComponents.INK_REMAINING.get(), newInk);
|
||||
}
|
||||
|
||||
// All validation and output construction is complete before changing either input.
|
||||
if (page.getCount() == 1) {
|
||||
player.setItemInHand(pageHand, panel);
|
||||
} else {
|
||||
page.shrink(1);
|
||||
giveOrDrop(player, panel);
|
||||
}
|
||||
if (vial.getCount() == 1) {
|
||||
player.setItemInHand(vialHand, spentVial);
|
||||
} else {
|
||||
vial.shrink(1);
|
||||
giveOrDrop(player, spentVial);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
private static InteractionHand other(InteractionHand hand) {
|
||||
return hand == InteractionHand.MAIN_HAND ? InteractionHand.OFF_HAND : InteractionHand.MAIN_HAND;
|
||||
}
|
||||
|
||||
private static void giveOrDrop(Player player, ItemStack stack) {
|
||||
if (!player.getInventory().add(stack)) {
|
||||
player.drop(stack, false);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -15,6 +15,9 @@
|
||||
"item.rivencraft.notebook": "Notebook",
|
||||
"item.rivencraft.notebook.pages": "Pages: %s/%s",
|
||||
"item.rivencraft.notebook.unresolved": "Unresolved pages: %s",
|
||||
"item.rivencraft.ink_vial": "Ink Vial",
|
||||
"item.rivencraft.ink_vial.remaining": "Ink: %s/%s",
|
||||
"item.rivencraft.ink_vial.usage": "Use with a blank Page in the other hand to prepare a Link Panel.",
|
||||
"screen.rivencraft.notebook.rename": "Rename selected page",
|
||||
"screen.rivencraft.notebook.search": "Search pages",
|
||||
"screen.rivencraft.notebook.rename_toggle": "Rename selected page (R); use left/right arrows to reorder",
|
||||
|
||||
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"parent": "minecraft:item/generated",
|
||||
"textures": {
|
||||
"layer0": "rivencraft:item/ink_vial"
|
||||
}
|
||||
}
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 1.7 KiB |
@@ -0,0 +1,3 @@
|
||||
{
|
||||
"animation": {}
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
{
|
||||
"type": "minecraft:crafting_shapeless",
|
||||
"category": "misc",
|
||||
"ingredients": [
|
||||
{ "item": "minecraft:black_dye" },
|
||||
{ "item": "minecraft:black_dye" },
|
||||
{ "item": "minecraft:glass_bottle" },
|
||||
{ "item": "minecraft:water_bucket" }
|
||||
],
|
||||
"result": {
|
||||
"count": 1,
|
||||
"id": "rivencraft:ink_vial"
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user