Rearrange armor slot positioning in the spellbook, turn the spell slots into actual functioning slots

This commit is contained in:
Sollace 2023-08-29 15:21:10 +01:00
parent f9b6cf75f4
commit 576e36df39
No known key found for this signature in database
GPG key ID: E52FACE7B5C773DB
11 changed files with 223 additions and 107 deletions

View file

@ -1,67 +0,0 @@
package com.minelittlepony.unicopia.client.gui.spellbook;
import java.util.List;
import org.joml.Vector4f;
import com.minelittlepony.common.client.gui.element.Button;
import com.minelittlepony.unicopia.ability.magic.spell.effect.CustomisedSpellType;
import com.mojang.blaze3d.systems.RenderSystem;
import net.minecraft.client.MinecraftClient;
import net.minecraft.client.gui.DrawContext;
import net.minecraft.client.gui.screen.ingame.HandledScreen;
import net.minecraft.client.item.TooltipContext;
import net.minecraft.client.render.item.ItemRenderer;
import net.minecraft.client.sound.SoundManager;
public class EquippedSpellSlot extends Button {
protected final ItemRenderer itemRenderer = MinecraftClient.getInstance().getItemRenderer();
private final CustomisedSpellType<?> spell;
public EquippedSpellSlot(int x, int y, CustomisedSpellType<?> spell) {
super(x, y, 16, 16);
this.spell = spell;
getStyle().setTooltip(() -> {
if (spell.isEmpty()) {
return List.of();
}
return spell.getDefaultStack().getTooltip(MinecraftClient.getInstance().player, TooltipContext.Default.BASIC);
});
}
@Override
public void renderButton(DrawContext context, int mouseX, int mouseY, float tickDelta) {
RenderSystem.setShaderColor(1, 1, 1, 1);
RenderSystem.enableBlend();
context.drawTexture(SpellbookScreen.SLOT, getX() - 8, getY() - 8, 0, 0, 32, 32, 32, 32);
Vector4f pos = new Vector4f(getX(), getY(), 0, 1);
pos.mul(context.getMatrices().peek().getPositionMatrix());
if (spell.isEmpty()) {
RenderSystem.setShaderColor(1, 1, 1, 0.3F);
context.drawTexture(SpellbookScreen.GEM, getX(), getY(), 0, 0, 16, 16, 16, 16);
RenderSystem.disableBlend();
RenderSystem.setShaderColor(1, 1, 1, 1);
} else {
RenderSystem.disableBlend();
RenderSystem.setShaderColor(1, 1, 1, 1);
drawItem(context, (int)pos.x, (int)pos.y);
}
if (isHovered()) {
HandledScreen.drawSlotHighlight(context, getX(), getY(), 0);
}
}
protected void drawItem(DrawContext context, int x, int y) {
context.drawItem(spell.getDefaultStack(), x, y);
}
@Override
public void playDownSound(SoundManager soundManager) {
}
}

View file

@ -12,7 +12,6 @@ import net.minecraft.client.font.TextRenderer;
import net.minecraft.client.gui.DrawContext; import net.minecraft.client.gui.DrawContext;
import net.minecraft.client.util.math.MatrixStack; import net.minecraft.client.util.math.MatrixStack;
import net.minecraft.text.Text; import net.minecraft.text.Text;
import net.minecraft.util.Hand;
import net.minecraft.util.Identifier; import net.minecraft.util.Identifier;
import net.minecraft.util.math.MathHelper; import net.minecraft.util.math.MathHelper;
@ -45,16 +44,6 @@ public class SpellbookProfilePageContent implements SpellbookChapterList.Content
.getStyle() .getStyle()
.setIcon(TribeButton.createSprite(pony.getSpecies(), 0, 0, halfSize)); .setIcon(TribeButton.createSprite(pony.getSpecies(), 0, 0, halfSize));
} }
float mainAngle = 90 * MathHelper.RADIANS_PER_DEGREE;
float offAngle = 60 * MathHelper.RADIANS_PER_DEGREE;
int radius = 75;
x += size / 4;
y += size / 3;
screen.addDrawable(new EquippedSpellSlot(x + (int)(Math.sin(mainAngle) * radius), y + (int)(Math.cos(mainAngle) * radius), pony.getCharms().getEquippedSpell(Hand.MAIN_HAND)));
screen.addDrawable(new EquippedSpellSlot(x + (int)(Math.sin(offAngle) * radius), y + (int)(Math.cos(offAngle) * radius), pony.getCharms().getEquippedSpell(Hand.OFF_HAND)));
} }
@Override @Override

View file

@ -1,5 +1,7 @@
package com.minelittlepony.unicopia.client.gui.spellbook; package com.minelittlepony.unicopia.client.gui.spellbook;
import java.util.ArrayList;
import java.util.List;
import java.util.Optional; import java.util.Optional;
import java.util.function.IntConsumer; import java.util.function.IntConsumer;
@ -11,6 +13,8 @@ import com.minelittlepony.common.client.gui.sprite.TextureSprite;
import com.minelittlepony.unicopia.Debug; import com.minelittlepony.unicopia.Debug;
import com.minelittlepony.unicopia.USounds; import com.minelittlepony.unicopia.USounds;
import com.minelittlepony.unicopia.Unicopia; import com.minelittlepony.unicopia.Unicopia;
import com.minelittlepony.unicopia.ability.magic.spell.effect.CustomisedSpellType;
import com.minelittlepony.unicopia.client.FlowingText;
import com.minelittlepony.unicopia.client.gui.*; import com.minelittlepony.unicopia.client.gui.*;
import com.minelittlepony.unicopia.client.gui.spellbook.SpellbookChapterList.*; import com.minelittlepony.unicopia.client.gui.spellbook.SpellbookChapterList.*;
import com.minelittlepony.unicopia.container.*; import com.minelittlepony.unicopia.container.*;
@ -18,7 +22,6 @@ import com.minelittlepony.unicopia.container.inventory.*;
import com.minelittlepony.unicopia.network.Channel; import com.minelittlepony.unicopia.network.Channel;
import com.minelittlepony.unicopia.network.MsgSpellbookStateChanged; import com.minelittlepony.unicopia.network.MsgSpellbookStateChanged;
import com.minelittlepony.unicopia.trinkets.TrinketSlotBackSprites; import com.minelittlepony.unicopia.trinkets.TrinketSlotBackSprites;
import com.minelittlepony.unicopia.trinkets.TrinketsDelegate;
import com.mojang.blaze3d.platform.GlStateManager; import com.mojang.blaze3d.platform.GlStateManager;
import com.mojang.blaze3d.systems.RenderSystem; import com.mojang.blaze3d.systems.RenderSystem;
@ -193,6 +196,32 @@ public class SpellbookScreen extends HandledScreen<SpellbookScreenHandler> imple
matrices.pop(); matrices.pop();
} }
@Override
protected void drawMouseoverTooltip(DrawContext context, int x, int y) {
context.getMatrices().push();
context.getMatrices().translate(0, 0, 200);
if (!(focusedSlot instanceof SpellSlot sp)) {
super.drawMouseoverTooltip(context, x, y);
return;
}
CustomisedSpellType<?> spell = sp.getSpell();
if (spell.isEmpty()) {
context.drawTooltip(textRenderer, Text.translatable("gui.unicopia.spellbook.empty_spell_slot"), x, y);
return;
}
List<Text> tooltip = new ArrayList<>();
tooltip.add(spell.type().getName());
tooltip.addAll(FlowingText.wrap(Text.translatable(spell.type().getTranslationKey() + ".lore").formatted(spell.type().getAffinity().getColor()), 180).toList());
context.drawTooltip(textRenderer, tooltip, x, y);
context.getMatrices().pop();
}
void drawSlots(DrawContext context, int mouseX, int mouseY, float delta) { void drawSlots(DrawContext context, int mouseX, int mouseY, float delta) {
MatrixStack matrices = context.getMatrices(); MatrixStack matrices = context.getMatrices();
matrices.push(); matrices.push();
@ -204,21 +233,37 @@ public class SpellbookScreen extends HandledScreen<SpellbookScreenHandler> imple
if (slot.isEnabled() && slot instanceof SpellbookSlot p) { if (slot.isEnabled() && slot instanceof SpellbookSlot p) {
context.drawTexture(SLOT, slot.x - 8, slot.y - 8, 0, 0, 32, 32, 32, 32); context.drawTexture(SLOT, slot.x - 8, slot.y - 8, 0, 0, 32, 32, 32, 32);
if (slot instanceof InputSlot) { if (slot.getStack().isEmpty()) {
RenderSystem.setShaderColor(1, 1, 1, 0.3F); Identifier foreground = p.getForegroundIdentifier();
context.drawTexture(GEM, slot.x, slot.y, 0, 0, 16, 16, 16, 16); if (foreground != null) {
RenderSystem.setShaderColor(1, 1, 1, 1); if (p.isTrinket()) {
foreground = TrinketSlotBackSprites.getBackSprite(foreground);
}
RenderSystem.setShaderColor(1, 1, 1, p.getBackSpriteOpacity());
context.drawTexture(foreground, slot.x, slot.y, 0, 0, 16, 16, 16, 16);
RenderSystem.setShaderColor(1, 1, 1, 1);
}
} }
if (!(p instanceof InventorySlot)) { if (slot instanceof SpellSlot sp) {
CustomisedSpellType<?> spell = sp.getSpell();
if (!spell.isEmpty()) {
RenderSystem.setShaderColor(1, 1, 1, 1);
context.getMatrices().push();
context.getMatrices().translate(0, 0, 260);
SpellIconRenderer.renderSpell(context, spell, slot.x, slot.y, 0.5F);
context.getMatrices().pop();
RenderSystem.enableBlend();
}
}
if (p.showTraits()) {
float weight = p.getWeight(); float weight = p.getWeight();
ItemTraitsTooltipRenderer.renderStackTraits(slot.getStack(), context, slot.x, slot.y, weight == 0 ? 1 : weight, delta, slot.id); ItemTraitsTooltipRenderer.renderStackTraits(slot.getStack(), context, slot.x, slot.y, weight == 0 ? 1 : weight, delta, slot.id);
RenderSystem.enableBlend(); RenderSystem.enableBlend();
} }
} }
if (slot.isEnabled() && slot instanceof TrinketsDelegate.SlotWithForeground fg && slot.getStack().isEmpty()) {
context.drawTexture(TrinketSlotBackSprites.getBackSprite(fg.getForegroundIdentifier()), slot.x, slot.y, 0, 0, 16, 16, 16, 16);
}
} }
RenderSystem.disableBlend(); RenderSystem.disableBlend();
RenderSystem.setShaderColor(1, 1, 1, 1); RenderSystem.setShaderColor(1, 1, 1, 1);

View file

@ -7,6 +7,7 @@ import org.jetbrains.annotations.Nullable;
import com.minelittlepony.unicopia.ability.magic.spell.crafting.SpellbookRecipe; import com.minelittlepony.unicopia.ability.magic.spell.crafting.SpellbookRecipe;
import com.minelittlepony.unicopia.container.inventory.*; import com.minelittlepony.unicopia.container.inventory.*;
import com.minelittlepony.unicopia.entity.player.Pony;
import com.minelittlepony.unicopia.item.UItems; import com.minelittlepony.unicopia.item.UItems;
import com.minelittlepony.unicopia.item.URecipes; import com.minelittlepony.unicopia.item.URecipes;
import com.minelittlepony.unicopia.trinkets.TrinketsDelegate; import com.minelittlepony.unicopia.trinkets.TrinketsDelegate;
@ -27,6 +28,7 @@ import net.minecraft.screen.ScreenHandler;
import net.minecraft.screen.ScreenHandlerContext; import net.minecraft.screen.ScreenHandlerContext;
import net.minecraft.screen.slot.Slot; import net.minecraft.screen.slot.Slot;
import net.minecraft.server.network.ServerPlayerEntity; import net.minecraft.server.network.ServerPlayerEntity;
import net.minecraft.util.Hand;
import net.minecraft.util.Identifier; import net.minecraft.util.Identifier;
public class SpellbookScreenHandler extends ScreenHandler { public class SpellbookScreenHandler extends ScreenHandler {
@ -89,18 +91,32 @@ public class SpellbookScreenHandler extends ScreenHandler {
addSlot(gemSlot = new InputSlot(this, input, MAX_INGREDIENTS, gemPos.get(0))); addSlot(gemSlot = new InputSlot(this, input, MAX_INGREDIENTS, gemPos.get(0)));
final int slotSpacing = 18;
final int halfSpacing = slotSpacing / 2;
for (int i = 0; i < 9; ++i) { for (int i = 0; i < 9; ++i) {
addSlot(new Slot(inventory, i, 121 + i * 18, 195)); addSlot(new Slot(inventory, i, 121 + i * slotSpacing, 195));
} }
final int inventoryX = 225;
final int inventoryY = 45;
for (int i = 0; i < PlayerInventory.MAIN_SIZE - 9; ++i) { for (int i = 0; i < PlayerInventory.MAIN_SIZE - 9; ++i) {
int x = i % 5; int x = i % 4;
int y = i / 5; int y = i / 4;
addSlot(new InventorySlot(this, inventory, i + 9, 225 + x * 20, 50 + y * 20)); addSlot(new InventorySlot(this, inventory, i + 9, inventoryX + x * slotSpacing, inventoryY + y * slotSpacing));
} }
final int armorX = 330;
final int armorY = inventoryY + slotSpacing;
final int equipmentY = armorY + halfSpacing;
final int leftHandX = armorX - slotSpacing;
final int rightHandX = armorX + slotSpacing;
for (int i = 0; i < 4; i++) { for (int i = 0; i < 4; i++) {
final EquipmentSlot eq = EquipmentSlot.values()[5 - i]; final EquipmentSlot eq = EquipmentSlot.values()[5 - i];
addSlot(new InventorySlot(this, inventory, PlayerInventory.OFF_HAND_SLOT - i - 1, 335, 50 + (i * 20)) { addSlot(new InventorySlot(this, inventory, PlayerInventory.OFF_HAND_SLOT - i - 1, armorX, armorY + (i * slotSpacing)) {
@Override @Override
public int getMaxItemCount() { public int getMaxItemCount() {
return 1; return 1;
@ -126,21 +142,23 @@ public class SpellbookScreenHandler extends ScreenHandler {
} }
}); });
} }
addSlot(new InventorySlot(this, inventory, PlayerInventory.OFF_HAND_SLOT, rightHandX, equipmentY + slotSpacing) {
TrinketsDelegate.getInstance().createSlot(this, inv.player, TrinketsDelegate.FACE, 0, 336 + 20, 60).ifPresent(this::addSlot);
TrinketsDelegate.getInstance().createSlot(this, inv.player, TrinketsDelegate.NECKLACE, 0, 336 + 20, 60 + 20).ifPresent(this::addSlot);
TrinketsDelegate.getInstance().createSlot(this, inv.player, TrinketsDelegate.MAINHAND, 0, 336 - 10, 155).ifPresent(this::addSlot);
TrinketsDelegate.getInstance().createSlot(this, inv.player, TrinketsDelegate.OFFHAND, 0, 336 + 20, 155).ifPresent(this::addSlot);
addSlot(new InventorySlot(this, inventory, PlayerInventory.OFF_HAND_SLOT, 342, 140) {
@Override @Override
public Pair<Identifier, Identifier> getBackgroundSprite() { public Pair<Identifier, Identifier> getBackgroundSprite() {
return Pair.of(PlayerScreenHandler.BLOCK_ATLAS_TEXTURE, PlayerScreenHandler.EMPTY_OFFHAND_ARMOR_SLOT); return Pair.of(PlayerScreenHandler.BLOCK_ATLAS_TEXTURE, PlayerScreenHandler.EMPTY_OFFHAND_ARMOR_SLOT);
} }
}); });
TrinketsDelegate.getInstance().createSlot(this, inv.player, TrinketsDelegate.FACE, 0, rightHandX, inventoryY + slotSpacing * 6).ifPresent(this::addSlot);
TrinketsDelegate.getInstance().createSlot(this, inv.player, TrinketsDelegate.NECKLACE, 0, leftHandX, equipmentY + slotSpacing).ifPresent(this::addSlot);
TrinketsDelegate.getInstance().createSlot(this, inv.player, TrinketsDelegate.MAINHAND, 0, leftHandX, equipmentY).ifPresent(this::addSlot);
TrinketsDelegate.getInstance().createSlot(this, inv.player, TrinketsDelegate.OFFHAND, 0, rightHandX, equipmentY).ifPresent(this::addSlot);
addSlot(outputSlot = new OutputSlot(this, inventory.player, input, result, 0, gemPos.get(0))); addSlot(outputSlot = new OutputSlot(this, inventory.player, input, result, 0, gemPos.get(0)));
addSlot(new SpellSlot(this, Pony.of(inventory.player), Hand.MAIN_HAND, inventory, 0, leftHandX, equipmentY - slotSpacing));
addSlot(new SpellSlot(this, Pony.of(inventory.player), Hand.OFF_HAND, inventory, 0, rightHandX, equipmentY - slotSpacing));
onContentChanged(input); onContentChanged(input);
} }

View file

@ -1,9 +1,12 @@
package com.minelittlepony.unicopia.container.inventory; package com.minelittlepony.unicopia.container.inventory;
import org.jetbrains.annotations.Nullable;
import com.minelittlepony.unicopia.container.*; import com.minelittlepony.unicopia.container.*;
import net.minecraft.inventory.Inventory; import net.minecraft.inventory.Inventory;
import net.minecraft.screen.slot.Slot; import net.minecraft.screen.slot.Slot;
import net.minecraft.util.Identifier;
public class InputSlot extends Slot implements SpellbookSlot { public class InputSlot extends Slot implements SpellbookSlot {
private final SpellbookScreenHandler handler; private final SpellbookScreenHandler handler;
@ -29,4 +32,10 @@ public class InputSlot extends Slot implements SpellbookSlot {
public boolean isEnabled() { public boolean isEnabled() {
return handler.canShowSlots(SlotType.CRAFTING) && !handler.outputSlot.isEnabled(); return handler.canShowSlots(SlotType.CRAFTING) && !handler.outputSlot.isEnabled();
} }
@Override
@Nullable
public Identifier getForegroundIdentifier() {
return GEM;
}
} }

View file

@ -17,4 +17,14 @@ public class InventorySlot extends Slot implements SpellbookSlot {
public boolean isEnabled() { public boolean isEnabled() {
return handler.canShowSlots(SlotType.INVENTORY); return handler.canShowSlots(SlotType.INVENTORY);
} }
@Override
public boolean showTraits() {
return false;
}
@Override
public float getBackSpriteOpacity() {
return 1F;
}
} }

View file

@ -0,0 +1,87 @@
package com.minelittlepony.unicopia.container.inventory;
import org.jetbrains.annotations.Nullable;
import com.minelittlepony.unicopia.ability.magic.spell.effect.CustomisedSpellType;
import com.minelittlepony.unicopia.ability.magic.spell.effect.SpellType;
import com.minelittlepony.unicopia.container.SpellbookScreenHandler;
import com.minelittlepony.unicopia.entity.player.Pony;
import com.minelittlepony.unicopia.item.EnchantableItem;
import com.minelittlepony.unicopia.item.UItems;
import net.minecraft.inventory.Inventory;
import net.minecraft.item.ItemStack;
import net.minecraft.screen.slot.Slot;
import net.minecraft.util.Hand;
import net.minecraft.util.Identifier;
public class SpellSlot extends Slot implements SpellbookSlot {
private final SpellbookScreenHandler handler;
private final Pony pony;
private final Hand hand;
public SpellSlot(SpellbookScreenHandler handler, Pony pony, Hand hand, Inventory inventory, int index, int x, int y) {
super(inventory, index, x, y);
this.handler = handler;
this.pony = pony;
this.hand = hand;
}
public CustomisedSpellType<?> getSpell() {
return pony.getCharms().getEquippedSpell(hand);
}
@Override
public int getMaxItemCount() {
return 1;
}
@Override
public boolean isEnabled() {
return handler.canShowSlots(SlotType.INVENTORY) && !handler.canShowSlots(SlotType.CRAFTING);
}
@Override
public boolean canInsert(ItemStack stack) {
return stack.isOf(UItems.GEMSTONE);
}
@Override
public ItemStack getStack() {
var spell = getSpell();
return spell.isEmpty() ? UItems.GEMSTONE.getDefaultStack() : spell.getDefaultStack();
}
@Override
public void setStackNoCallbacks(ItemStack stack) {
if (stack.isEmpty()) {
pony.getCharms().equipSpell(hand, SpellType.EMPTY_KEY.withTraits());
} else {
var result = EnchantableItem.consumeSpell(stack, pony.asEntity(), null, true);
pony.getCharms().equipSpell(hand, result.getValue());
}
}
@Override
public ItemStack takeStack(int amount) {
return ItemStack.EMPTY;
}
@Override
public void markDirty() {
pony.setDirty();
}
@Override
@Nullable
public Identifier getForegroundIdentifier() {
return GEM;
}
@Override
public boolean showTraits() {
return false;
}
}

View file

@ -1,6 +1,15 @@
package com.minelittlepony.unicopia.container.inventory; package com.minelittlepony.unicopia.container.inventory;
import org.jetbrains.annotations.Nullable;
import com.minelittlepony.unicopia.Unicopia;
import net.minecraft.util.Identifier;
public interface SpellbookSlot { public interface SpellbookSlot {
Identifier EMPTY_TEXTURE = Unicopia.id("transparent");
Identifier GEM = Unicopia.id("textures/item/gemstone.png");
float CENTER_FACTOR = 0; float CENTER_FACTOR = 0;
float NEAR_FACTOR = 1; float NEAR_FACTOR = 1;
float MIDDLE_FACTOR = 0.6F; float MIDDLE_FACTOR = 0.6F;
@ -9,4 +18,21 @@ public interface SpellbookSlot {
default float getWeight() { default float getWeight() {
return CENTER_FACTOR; return CENTER_FACTOR;
} }
default float getBackSpriteOpacity() {
return 0.3F;
}
@Nullable
default Identifier getForegroundIdentifier() {
return null;
}
default boolean isTrinket() {
return false;
}
default boolean showTraits() {
return true;
}
} }

View file

@ -9,7 +9,7 @@ import net.minecraft.entity.player.PlayerEntity;
import net.minecraft.item.ItemStack; import net.minecraft.item.ItemStack;
import net.minecraft.util.Identifier; import net.minecraft.util.Identifier;
class SpellbookTrinketSlot extends InventorySlot implements TrinketsDelegate.SlotWithForeground { class SpellbookTrinketSlot extends InventorySlot {
private final SurvivalTrinketSlot slot; private final SurvivalTrinketSlot slot;
public SpellbookTrinketSlot(SpellbookScreenHandler handler, TrinketInventory inventory, int index, int x, int y, SlotGroup group) { public SpellbookTrinketSlot(SpellbookScreenHandler handler, TrinketInventory inventory, int index, int x, int y, SlotGroup group) {
@ -81,4 +81,9 @@ class SpellbookTrinketSlot extends InventorySlot implements TrinketsDelegate.Slo
public Identifier getForegroundIdentifier() { public Identifier getForegroundIdentifier() {
return slot.getBackgroundIdentifier(); return slot.getBackgroundIdentifier();
} }
@Override
public boolean isTrinket() {
return true;
}
} }

View file

@ -5,7 +5,6 @@ import java.util.stream.Collectors;
import java.util.stream.Stream; import java.util.stream.Stream;
import com.minelittlepony.unicopia.EntityConvertable; import com.minelittlepony.unicopia.EntityConvertable;
import com.minelittlepony.unicopia.Unicopia;
import com.minelittlepony.unicopia.container.SpellbookScreenHandler; import com.minelittlepony.unicopia.container.SpellbookScreenHandler;
import net.fabricmc.loader.api.FabricLoader; import net.fabricmc.loader.api.FabricLoader;
import net.minecraft.entity.EquipmentSlot; import net.minecraft.entity.EquipmentSlot;
@ -112,10 +111,4 @@ public interface TrinketsDelegate {
TrinketsDelegate.getInstance().setEquippedStack(asEntity(), slot, stack); TrinketsDelegate.getInstance().setEquippedStack(asEntity(), slot, stack);
} }
} }
interface SlotWithForeground {
Identifier EMPTY_TEXTURE = Unicopia.id("transparent");
Identifier getForegroundIdentifier();
}
} }

View file

@ -477,6 +477,7 @@
"gui.unicopia.tribe_selection.join": "Join Tribe", "gui.unicopia.tribe_selection.join": "Join Tribe",
"gui.unicopia.tribe_selection.cancel": "Go Back", "gui.unicopia.tribe_selection.cancel": "Go Back",
"gui.unicopia.spellbook.empty_spell_slot": "Empty Spell Slot",
"gui.unicopia.spellbook.page.inventory": "Inventory", "gui.unicopia.spellbook.page.inventory": "Inventory",
"gui.unicopia.spellbook.page.recipes": "Recipes", "gui.unicopia.spellbook.page.recipes": "Recipes",
"gui.unicopia.spellbook.page.recipes.empty": "0 Recipes Unlocked", "gui.unicopia.spellbook.page.recipes.empty": "0 Recipes Unlocked",