Handbook rewrite part 1: Formatting and JSON-ification!
- Everything is now defined in a JSON file - Formatting is now done dynamically and content wraps properly no matter what font is used - Added inline hyperlinks - Added a movable bookmark for quick access to a specific page - Added next section and previous section buttons to flip through entire sections at once - Added a contents button visible on every page, which navigates directly to the main contents
This commit is contained in:
@@ -1,42 +0,0 @@
|
||||
package electroblob.wizardry.client.gui;
|
||||
|
||||
import electroblob.wizardry.Wizardry;
|
||||
import electroblob.wizardry.util.WizardryUtilities;
|
||||
import net.minecraft.client.Minecraft;
|
||||
import net.minecraft.client.gui.GuiButton;
|
||||
import net.minecraft.client.resources.I18n;
|
||||
import net.minecraftforge.fml.relauncher.Side;
|
||||
import net.minecraftforge.fml.relauncher.SideOnly;
|
||||
|
||||
@SideOnly(Side.CLIENT)
|
||||
class GuiButtonApply extends GuiButton {
|
||||
|
||||
public GuiButtonApply(int id, int x, int y){
|
||||
super(id, x, y, 32, 16, I18n.format("container." + Wizardry.MODID + ":arcane_workbench.apply"));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void drawButton(Minecraft minecraft, int mouseX, int mouseY, float partialTicks){
|
||||
|
||||
// Whether the button is highlighted
|
||||
this.hovered = mouseX >= this.x && mouseY >= this.y && mouseX < this.x + this.width && mouseY < this.y + this.height;
|
||||
|
||||
int k = 36;
|
||||
int l = 220;
|
||||
int colour = 14737632;
|
||||
|
||||
if(this.enabled){
|
||||
if(this.hovered){
|
||||
k += this.width * 2;
|
||||
colour = 16777120;
|
||||
}
|
||||
}else{
|
||||
k += this.width;
|
||||
colour = 10526880;
|
||||
}
|
||||
|
||||
WizardryUtilities.drawTexturedRect(this.x, this.y, k, l, this.width, this.height, 256, 256);
|
||||
this.drawCenteredString(minecraft.fontRenderer, this.displayString, this.x + this.width / 2,
|
||||
this.y + (this.height - 8) / 2, colour);
|
||||
}
|
||||
}
|
||||
@@ -6,7 +6,7 @@ import net.minecraftforge.fml.relauncher.Side;
|
||||
import net.minecraftforge.fml.relauncher.SideOnly;
|
||||
|
||||
@SideOnly(Side.CLIENT)
|
||||
class GuiButtonInvisible extends GuiButton {
|
||||
public class GuiButtonInvisible extends GuiButton {
|
||||
|
||||
public GuiButtonInvisible(int id, int x, int y, int width, int height){
|
||||
super(id, x, y, width, height, "");
|
||||
|
||||
@@ -1,47 +0,0 @@
|
||||
package electroblob.wizardry.client.gui;
|
||||
|
||||
import electroblob.wizardry.Wizardry;
|
||||
import electroblob.wizardry.util.WizardryUtilities;
|
||||
import net.minecraft.client.Minecraft;
|
||||
import net.minecraft.client.gui.GuiButton;
|
||||
import net.minecraft.client.renderer.GlStateManager;
|
||||
import net.minecraft.util.ResourceLocation;
|
||||
import net.minecraftforge.fml.relauncher.Side;
|
||||
import net.minecraftforge.fml.relauncher.SideOnly;
|
||||
|
||||
@SideOnly(Side.CLIENT)
|
||||
class GuiButtonTurnPage extends GuiButton {
|
||||
|
||||
/** True for pointing right (next page), false for pointing left (previous page). */
|
||||
private final boolean nextPage;
|
||||
|
||||
private static final ResourceLocation texture = new ResourceLocation(Wizardry.MODID, "textures/gui/handbook.png");
|
||||
|
||||
public GuiButtonTurnPage(int id, int x, int y, boolean isNextPage){
|
||||
super(id, x, y, 23, 13, "");
|
||||
this.nextPage = isNextPage;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void drawButton(Minecraft minecraft, int mouseX, int mouseY, float partialTicks){
|
||||
|
||||
if(this.visible){
|
||||
|
||||
boolean flag = mouseX >= this.x && mouseY >= this.y && mouseX < this.x + this.width && mouseY < this.y + this.height;
|
||||
GlStateManager.color(1.0F, 1.0F, 1.0F, 1.0F);
|
||||
minecraft.getTextureManager().bindTexture(texture);
|
||||
int k = 0;
|
||||
int l = 192;
|
||||
|
||||
if(flag){
|
||||
k += 23;
|
||||
}
|
||||
|
||||
if(!this.nextPage){
|
||||
l += 13;
|
||||
}
|
||||
|
||||
WizardryUtilities.drawTexturedRect(this.x, this.y, k, l, 23, 13, 288, 256);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,695 +0,0 @@
|
||||
package electroblob.wizardry.client.gui;
|
||||
|
||||
import java.io.BufferedReader;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStreamReader;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
import org.apache.commons.lang3.tuple.ImmutablePair;
|
||||
import org.apache.commons.lang3.tuple.Pair;
|
||||
import org.lwjgl.input.Keyboard;
|
||||
import org.lwjgl.opengl.GL11;
|
||||
|
||||
import electroblob.wizardry.Wizardry;
|
||||
import electroblob.wizardry.client.ClientProxy;
|
||||
import electroblob.wizardry.constants.Constants;
|
||||
import electroblob.wizardry.constants.Element;
|
||||
import electroblob.wizardry.constants.Tier;
|
||||
import electroblob.wizardry.registry.WizardryBlocks;
|
||||
import electroblob.wizardry.registry.WizardryItems;
|
||||
import electroblob.wizardry.util.WizardryUtilities;
|
||||
import net.minecraft.client.Minecraft;
|
||||
import net.minecraft.client.gui.GuiButton;
|
||||
import net.minecraft.client.gui.GuiScreen;
|
||||
import net.minecraft.client.renderer.BufferBuilder;
|
||||
import net.minecraft.client.renderer.GlStateManager;
|
||||
import net.minecraft.client.renderer.RenderHelper;
|
||||
import net.minecraft.client.renderer.Tessellator;
|
||||
import net.minecraft.client.renderer.vertex.DefaultVertexFormats;
|
||||
import net.minecraft.init.Blocks;
|
||||
import net.minecraft.init.Items;
|
||||
import net.minecraft.item.ItemStack;
|
||||
import net.minecraft.util.NonNullList;
|
||||
import net.minecraft.util.ResourceLocation;
|
||||
|
||||
public class GuiWizardHandbook extends GuiScreen {
|
||||
|
||||
private int xSize, ySize;
|
||||
private int pageNumber = 0;
|
||||
|
||||
private static final int PAGE_WIDTH = 120;
|
||||
/**
|
||||
* The integer colour for black passed into the font renderer methods. This used to be 0 but that's now white for
|
||||
* some reason, so I've made a it a constant in case it changes again.
|
||||
*/
|
||||
// I think this is actually ever-so-slightly lighter than pure black, but the difference is unnoticeable.
|
||||
private static final int BLACK = 1;
|
||||
|
||||
public static final ResourceLocation regularHandbook = new ResourceLocation(Wizardry.MODID, "textures/gui/handbook.png");
|
||||
public static final ResourceLocation ore = new ResourceLocation(Wizardry.MODID, "textures/gui/ore_picture.png");
|
||||
public static final ResourceLocation crystal = new ResourceLocation(Wizardry.MODID, "textures/items/magic_crystal.png");
|
||||
public static final ResourceLocation workbenchGui = new ResourceLocation(Wizardry.MODID, "textures/gui/arcane_workbench.png");
|
||||
public static final ResourceLocation craftingGrids = new ResourceLocation(Wizardry.MODID, "textures/gui/handbook_recipes.png");
|
||||
|
||||
private List<ArrayList<String>> text;
|
||||
private List<Section> sections;
|
||||
|
||||
private static final List<Pair<ItemStack, NonNullList<NonNullList<ItemStack>>>> RECIPES = new ArrayList<>();
|
||||
|
||||
private int guiPage, imagePage;
|
||||
|
||||
public GuiWizardHandbook(){
|
||||
super();
|
||||
xSize = 288;
|
||||
ySize = 180;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void drawScreen(int mouseX, int mouseY, float par3){
|
||||
|
||||
int xPos = this.width / 2 - xSize / 2;
|
||||
int yPos = this.height / 2 - this.ySize / 2;
|
||||
|
||||
// Tests for crafting recipes section
|
||||
if(pageNumber >= (sections.get(sections.size() - 1).pageNumber - 1) / 2
|
||||
&& pageNumber < (sections.get(sections.size() - 1).pageNumber - 1) / 2 + 4){
|
||||
Minecraft.getMinecraft().renderEngine.bindTexture(craftingGrids);
|
||||
}else{
|
||||
Minecraft.getMinecraft().renderEngine.bindTexture(regularHandbook);
|
||||
}
|
||||
|
||||
WizardryUtilities.drawTexturedRect(xPos, yPos, 0, 0, xSize, ySize, xSize, 256);
|
||||
|
||||
// Arcane workbench gui picture
|
||||
if(pageNumber == (this.guiPage - 1) / 2){
|
||||
Minecraft.getMinecraft().renderEngine.bindTexture(workbenchGui);
|
||||
this.drawTexturedModalRect(this.guiPage % 2 == 1 ? xPos + 17 : this.width / 2 + 7, yPos + 14, 28, 12, 120,
|
||||
118);
|
||||
}
|
||||
|
||||
// Magic crystal and crystal ore images
|
||||
if(pageNumber == (this.imagePage - 1) / 2){
|
||||
|
||||
Minecraft.getMinecraft().renderEngine.bindTexture(ore);
|
||||
WizardryUtilities.drawTexturedRect(this.imagePage % 2 == 1 ? xPos + 17 : this.width / 2 + 7, yPos + 80, 0,
|
||||
0, 64, 64, 64, 64);
|
||||
|
||||
Minecraft.getMinecraft().renderEngine.bindTexture(crystal);
|
||||
drawTexturedStretchedRect(this.imagePage % 2 == 1 ? xPos + 17 + 64 : this.width / 2 + 7 + 62, yPos + 80, 0,
|
||||
0, 64, 64, 1, 1);
|
||||
|
||||
}
|
||||
|
||||
this.fontRenderer.drawString("" + (pageNumber * 2 + 1), xPos + xSize / 4 - 3, yPos + ySize - 20, 0);
|
||||
this.fontRenderer.drawString("" + (pageNumber * 2 + 2), xPos + 3 * xSize / 4 - 5, yPos + ySize - 20, 0);
|
||||
|
||||
super.drawScreen(mouseX, mouseY, par3);
|
||||
|
||||
int lineNumber = 0;
|
||||
|
||||
if(pageNumber == 1){
|
||||
for(Section s : sections){
|
||||
s.drawContents();
|
||||
}
|
||||
}else{
|
||||
for(Section s : sections){
|
||||
s.hideButton();
|
||||
}
|
||||
}
|
||||
|
||||
for(String paragraph : text.get(pageNumber * 2)){
|
||||
|
||||
this.fontRenderer.drawSplitString(paragraph, xPos + 17,
|
||||
yPos + 14 + lineNumber * this.fontRenderer.FONT_HEIGHT, PAGE_WIDTH, BLACK);
|
||||
|
||||
List<String> list = new ArrayList<String>(
|
||||
this.fontRenderer.listFormattedStringToWidth(paragraph, GuiWizardHandbook.PAGE_WIDTH));
|
||||
|
||||
lineNumber += list.size();
|
||||
}
|
||||
|
||||
lineNumber = 0;
|
||||
|
||||
// Prevents crash when the last page is blank (and hence is not in the list of pages)
|
||||
if(text.size() > pageNumber * 2 + 1){
|
||||
for(String paragraph : text.get(pageNumber * 2 + 1)){
|
||||
|
||||
// First page is centred
|
||||
if(pageNumber == 0){
|
||||
int startX = this.width / 2 + 7 + PAGE_WIDTH / 2
|
||||
- this.fontRenderer.getStringWidth(paragraph) / 2;
|
||||
this.fontRenderer.drawSplitString(paragraph, startX,
|
||||
yPos + 14 + lineNumber * this.fontRenderer.FONT_HEIGHT, PAGE_WIDTH, BLACK);
|
||||
}else{
|
||||
this.fontRenderer.drawSplitString(paragraph, this.width / 2 + 7,
|
||||
yPos + 14 + lineNumber * this.fontRenderer.FONT_HEIGHT, PAGE_WIDTH, BLACK);
|
||||
}
|
||||
|
||||
List<String> list = new ArrayList<String>(
|
||||
this.fontRenderer.listFormattedStringToWidth(paragraph, GuiWizardHandbook.PAGE_WIDTH));
|
||||
|
||||
lineNumber += list.size();
|
||||
}
|
||||
}
|
||||
|
||||
// Which page of the recipes this is
|
||||
int recipePage = pageNumber - (sections.get(sections.size() - 1).pageNumber - 1) / 2;
|
||||
|
||||
if(recipePage >= 0 && recipePage < 4){
|
||||
// 4 recipes per page, hence the recipePage*4
|
||||
this.renderCraftingRecipe(xPos + 23, yPos + 39, mouseX, mouseY, RECIPES.get(recipePage*4).getRight(), RECIPES.get(recipePage*4).getLeft());
|
||||
this.renderCraftingRecipe(xPos + 23, yPos + 98, mouseX, mouseY, RECIPES.get(recipePage*4+1).getRight(), RECIPES.get(recipePage*4+1).getLeft());
|
||||
this.renderCraftingRecipe(xPos + 156, yPos + 39, mouseX, mouseY, RECIPES.get(recipePage*4+2).getRight(), RECIPES.get(recipePage*4+2).getLeft());
|
||||
this.renderCraftingRecipe(xPos + 156, yPos + 98, mouseX, mouseY, RECIPES.get(recipePage*4+3).getRight(), RECIPES.get(recipePage*4+3).getLeft());
|
||||
|
||||
// Tooltips are rendered after recipes to prevent tooltips on the left appearing behind items on the right.
|
||||
this.renderCraftingTooltips(xPos + 23, yPos + 39, mouseX, mouseY, RECIPES.get(recipePage*4).getRight(), RECIPES.get(recipePage*4).getLeft());
|
||||
this.renderCraftingTooltips(xPos + 23, yPos + 98, mouseX, mouseY, RECIPES.get(recipePage*4+1).getRight(), RECIPES.get(recipePage*4+1).getLeft());
|
||||
this.renderCraftingTooltips(xPos + 156, yPos + 39, mouseX, mouseY, RECIPES.get(recipePage*4+2).getRight(), RECIPES.get(recipePage*4+2).getLeft());
|
||||
this.renderCraftingTooltips(xPos + 156, yPos + 98, mouseX, mouseY, RECIPES.get(recipePage*4+3).getRight(), RECIPES.get(recipePage*4+3).getLeft());
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// TODO: In 1.12, this all needs redoing nicely. With the crafting system halfway through changing in 1.11.2, this
|
||||
// isn't worth doing until then.
|
||||
|
||||
private void renderCraftingRecipe(int xPos, int yPos, int mouseX, int mouseY, NonNullList<NonNullList<ItemStack>> craftingGrid,
|
||||
ItemStack craftingResult){
|
||||
|
||||
GlStateManager.pushMatrix();
|
||||
RenderHelper.enableGUIStandardItemLighting();
|
||||
GlStateManager.disableLighting();
|
||||
GlStateManager.enableRescaleNormal();
|
||||
GlStateManager.enableColorMaterial();
|
||||
GlStateManager.enableLighting();
|
||||
itemRender.zLevel = 100.0F;
|
||||
|
||||
for(int i = 0; i < craftingGrid.size(); i++){
|
||||
for(int j = 0; j < craftingGrid.get(i).size(); j++){
|
||||
ItemStack stack = craftingGrid.get(i).get(j);
|
||||
if(!stack.isEmpty()){
|
||||
itemRender.renderItemAndEffectIntoGUI(stack, xPos + 18 * i, yPos + 18 * j);
|
||||
itemRender.renderItemOverlays(this.fontRenderer, stack, xPos + 18 * i,
|
||||
yPos + 18 * j);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if(!craftingResult.isEmpty()){
|
||||
itemRender.renderItemAndEffectIntoGUI(craftingResult, xPos + 86, yPos + 18);
|
||||
itemRender.renderItemOverlays(this.fontRenderer, craftingResult, xPos + 86, yPos + 18);
|
||||
}
|
||||
|
||||
GlStateManager.popMatrix();
|
||||
GlStateManager.enableLighting();
|
||||
GlStateManager.enableDepth();
|
||||
RenderHelper.enableStandardItemLighting();
|
||||
|
||||
}
|
||||
|
||||
private void renderCraftingTooltips(int xPos, int yPos, int mouseX, int mouseY, NonNullList<NonNullList<ItemStack>> craftingGrid,
|
||||
ItemStack craftingResult){
|
||||
|
||||
int guiLeft = this.width / 2 - xSize / 2;
|
||||
int guiTop = this.height / 2 - this.ySize / 2;
|
||||
|
||||
GlStateManager.pushMatrix();
|
||||
RenderHelper.enableGUIStandardItemLighting();
|
||||
GlStateManager.disableLighting();
|
||||
GlStateManager.enableRescaleNormal();
|
||||
GlStateManager.enableColorMaterial();
|
||||
itemRender.zLevel = 0.0F;
|
||||
GlStateManager.disableLighting();
|
||||
|
||||
for(int i = 0; i < craftingGrid.size(); i++){
|
||||
for(int j = 0; j < craftingGrid.get(i).size(); j++){
|
||||
ItemStack stack = craftingGrid.get(i).get(j);
|
||||
if(!stack.isEmpty()
|
||||
&& isPointInRegion(xPos + 18 * i, yPos + 18 * j, 16, 16, mouseX + guiLeft, mouseY + guiTop)){
|
||||
this.renderToolTip(stack, mouseX, mouseY);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if(!craftingResult.isEmpty() && isPointInRegion(xPos + 86, yPos + 18, 16, 16, mouseX + guiLeft, mouseY + guiTop)){
|
||||
this.renderToolTip(craftingResult, mouseX, mouseY);
|
||||
}
|
||||
|
||||
GlStateManager.popMatrix();
|
||||
GlStateManager.enableLighting();
|
||||
GlStateManager.enableDepth();
|
||||
RenderHelper.enableStandardItemLighting();
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
public void initGui(){
|
||||
|
||||
super.initGui();
|
||||
Keyboard.enableRepeatEvents(true);
|
||||
|
||||
int nextButtonId = 0;
|
||||
|
||||
this.buttonList.clear();
|
||||
this.buttonList.add(new GuiButtonTurnPage(nextButtonId++, this.width / 2 + this.xSize / 2 - 22 - 23,
|
||||
this.height / 2 + this.ySize / 2 - 10 - 13, true));
|
||||
this.buttonList.add(new GuiButtonTurnPage(nextButtonId++, this.width / 2 - this.xSize / 2 + 21,
|
||||
this.height / 2 + this.ySize / 2 - 10 - 13, false));
|
||||
|
||||
text = new ArrayList<ArrayList<String>>(1);
|
||||
sections = new ArrayList<Section>(1);
|
||||
|
||||
BufferedReader bufferedreader = null;
|
||||
|
||||
String textFilepath = Wizardry.MODID + ":texts/handbook_"
|
||||
+ Minecraft.getMinecraft().getLanguageManager().getCurrentLanguage().getLanguageCode() + ".txt";
|
||||
|
||||
try{
|
||||
|
||||
bufferedreader = new BufferedReader(new InputStreamReader(
|
||||
this.mc.getResourceManager().getResource(new ResourceLocation(textFilepath)).getInputStream(),
|
||||
StandardCharsets.UTF_8));
|
||||
|
||||
}catch (IOException e){
|
||||
|
||||
Wizardry.logger.info(
|
||||
"Wizard handbook text file missing for the current language. Using default (English - US) instead.");
|
||||
|
||||
textFilepath = Wizardry.MODID + ":texts/handbook_en_us.txt";
|
||||
|
||||
try {
|
||||
|
||||
bufferedreader = new BufferedReader(new InputStreamReader(
|
||||
this.mc.getResourceManager().getResource(new ResourceLocation(textFilepath)).getInputStream(),
|
||||
StandardCharsets.UTF_8));
|
||||
|
||||
} catch (IOException x){
|
||||
Wizardry.logger.error("Couldn't find file: " + Wizardry.MODID + "/assets/texts/handbook_en_us.txt. The file may be"
|
||||
+ "missing; please try re-downloading and reinstalling Wizardry.", x);
|
||||
}
|
||||
}
|
||||
|
||||
if(bufferedreader != null){
|
||||
|
||||
try{
|
||||
|
||||
String paragraph = bufferedreader.readLine();
|
||||
ArrayList<String> page = new ArrayList<String>(1);
|
||||
|
||||
int linesPerPage = 16;
|
||||
|
||||
int lineNumber = 0;
|
||||
|
||||
while(paragraph != null){
|
||||
|
||||
// System.out.println(paragraph);
|
||||
|
||||
if(paragraph.contains("PAGEBREAK") || lineNumber >= linesPerPage){
|
||||
|
||||
text.add(page);
|
||||
|
||||
page = new ArrayList<String>(1);
|
||||
|
||||
lineNumber = 0;
|
||||
|
||||
if(paragraph.contains("PAGEBREAK")) paragraph = bufferedreader.readLine();
|
||||
|
||||
}else if(paragraph.contains("LINEBREAK")){
|
||||
|
||||
lineNumber++;
|
||||
|
||||
page.add("");
|
||||
|
||||
paragraph = bufferedreader.readLine();
|
||||
|
||||
}else if(paragraph.contains("SECTION")){
|
||||
|
||||
sections.add(
|
||||
new Section(paragraph.replace("SECTION ", ""), text.size() + 1, this.width / 2 + 7,
|
||||
this.height / 2 - this.ySize / 2 + 14
|
||||
+ (sections.size() + 2) * this.fontRenderer.FONT_HEIGHT,
|
||||
nextButtonId++));
|
||||
paragraph = bufferedreader.readLine();
|
||||
|
||||
}else if(paragraph.contains("IMAGE")){
|
||||
|
||||
if(paragraph.contains("WORKBENCH")){
|
||||
this.guiPage = text.size() + 1;
|
||||
}else if(paragraph.contains("CRYSTAL")){
|
||||
this.imagePage = text.size() + 1;
|
||||
}
|
||||
|
||||
paragraph = bufferedreader.readLine();
|
||||
|
||||
}else{
|
||||
|
||||
paragraph = paragraph.replaceAll("NEXT_SPELL_KEY", ClientProxy.NEXT_SPELL.getDisplayName());
|
||||
paragraph = paragraph.replaceAll("PREVIOUS_SPELL_KEY", ClientProxy.PREVIOUS_SPELL.getDisplayName());
|
||||
paragraph = paragraph.replaceAll("MANA_PER_CRYSTAL_MINUS_30", "" + (Constants.MANA_PER_CRYSTAL - 30));
|
||||
paragraph = paragraph.replaceAll("MANA_PER_CRYSTAL", "" + Constants.MANA_PER_CRYSTAL);
|
||||
paragraph = paragraph.replaceAll("BASIC_MAX_CHARGE", "" + Tier.BASIC.maxCharge);
|
||||
paragraph = paragraph.replaceAll("APPRENTICE_MAX_CHARGE", "" + Tier.APPRENTICE.maxCharge);
|
||||
paragraph = paragraph.replaceAll("ADVANCED_MAX_CHARGE", "" + Tier.ADVANCED.maxCharge);
|
||||
paragraph = paragraph.replaceAll("MASTER_MAX_CHARGE", "" + Tier.MASTER.maxCharge);
|
||||
paragraph = paragraph.replaceAll("BASIC_COLOUR", "\u00A77");
|
||||
paragraph = paragraph.replaceAll("APPRENTICE_COLOUR", Tier.APPRENTICE.getFormattingCode());
|
||||
paragraph = paragraph.replaceAll("ADVANCED_COLOUR", Tier.ADVANCED.getFormattingCode());
|
||||
paragraph = paragraph.replaceAll("MASTER_COLOUR", Tier.MASTER.getFormattingCode());
|
||||
paragraph = paragraph.replaceAll("FIRE_COLOUR", Element.FIRE.getFormattingCode());
|
||||
paragraph = paragraph.replaceAll("ICE_COLOUR", Element.ICE.getFormattingCode());
|
||||
paragraph = paragraph.replaceAll("LIGHTNING_COLOUR", Element.LIGHTNING.getFormattingCode());
|
||||
paragraph = paragraph.replaceAll("NECROMANCY_COLOUR", Element.NECROMANCY.getFormattingCode());
|
||||
paragraph = paragraph.replaceAll("EARTH_COLOUR", Element.EARTH.getFormattingCode());
|
||||
paragraph = paragraph.replaceAll("SORCERY_COLOUR", Element.SORCERY.getFormattingCode());
|
||||
paragraph = paragraph.replaceAll("HEALING_COLOUR", Element.HEALING.getFormattingCode());
|
||||
paragraph = paragraph.replaceAll("RESET_COLOUR", "\u00A70");
|
||||
paragraph = paragraph.replaceAll("MCVERSION", "1.12.2");
|
||||
paragraph = paragraph.replaceAll("VERSION", Wizardry.VERSION);
|
||||
|
||||
int linesInParagraph = this.fontRenderer
|
||||
.listFormattedStringToWidth(paragraph, GuiWizardHandbook.PAGE_WIDTH).size();
|
||||
|
||||
// Ignores empty lines at the top of a page.
|
||||
if(paragraph.isEmpty() && lineNumber == 0){
|
||||
|
||||
paragraph = bufferedreader.readLine();
|
||||
|
||||
// Normal paragraph, all on one page
|
||||
}else if(lineNumber + linesInParagraph <= linesPerPage){
|
||||
|
||||
page.add(paragraph);
|
||||
|
||||
lineNumber += linesInParagraph;
|
||||
|
||||
paragraph = bufferedreader.readLine();
|
||||
|
||||
// Paragraphs split across two pages (or more?)
|
||||
}else{
|
||||
|
||||
int linesInFirstPart = linesPerPage - lineNumber;
|
||||
|
||||
String paragraphFirstPart = "";
|
||||
String paragraphLastPart = "";
|
||||
|
||||
int i = 0;
|
||||
|
||||
List<String> strings = this.fontRenderer.listFormattedStringToWidth(paragraph,
|
||||
GuiWizardHandbook.PAGE_WIDTH);
|
||||
|
||||
for(Object s : strings){
|
||||
if(i < linesInFirstPart){
|
||||
paragraphFirstPart = paragraphFirstPart.concat((String)s + " ");
|
||||
}else{
|
||||
paragraphLastPart = paragraphLastPart.concat((String)s + " ");
|
||||
}
|
||||
i++;
|
||||
}
|
||||
|
||||
// System.out.println("Paragraph crosses page boundary; string split into: \"" +
|
||||
// paragraphFirstPart + "\" and \"" + paragraphLastPart + "\"");
|
||||
|
||||
page.add(paragraphFirstPart);
|
||||
|
||||
lineNumber += linesInFirstPart;
|
||||
|
||||
paragraph = paragraphLastPart;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
text.add(page);
|
||||
|
||||
}catch (IOException e){
|
||||
Wizardry.logger.error("Something went wrong reading file: " + textFilepath
|
||||
+ ". The file may be damaged; please try re-downloading and reinstalling wizardry.", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private class Section {
|
||||
|
||||
/** The integer text colour used for the section when it is moused over. Currently orange. */
|
||||
private static final int HIGHLIGHT_COLOUR = 0xdd4c1d;
|
||||
|
||||
String name;
|
||||
int pageNumber;
|
||||
int x, y;
|
||||
int buttonId;
|
||||
|
||||
Section(String name, int pageNumber, int x, int y, int id){
|
||||
this.name = name;
|
||||
this.pageNumber = pageNumber;
|
||||
this.x = x;
|
||||
this.y = y;
|
||||
this.buttonId = id;
|
||||
GuiWizardHandbook.this.buttonList.add(new GuiButtonInvisible(id, x, y, GuiWizardHandbook.PAGE_WIDTH,
|
||||
GuiWizardHandbook.this.fontRenderer.FONT_HEIGHT));
|
||||
}
|
||||
|
||||
void hideButton(){
|
||||
GuiWizardHandbook.this.buttonList.get(buttonId).visible = false;
|
||||
}
|
||||
|
||||
void drawContents(){
|
||||
|
||||
GuiWizardHandbook.this.buttonList.get(buttonId).visible = true;
|
||||
|
||||
GuiWizardHandbook.this.fontRenderer.drawString(name, x, y,
|
||||
GuiWizardHandbook.this.buttonList.get(buttonId).isMouseOver() ? HIGHLIGHT_COLOUR : BLACK);
|
||||
|
||||
int nameWidth = GuiWizardHandbook.this.fontRenderer.getStringWidth(name);
|
||||
|
||||
String dotsAndNumber = " " + this.pageNumber;
|
||||
|
||||
while(GuiWizardHandbook.this.fontRenderer.getStringWidth(dotsAndNumber) < GuiWizardHandbook.PAGE_WIDTH
|
||||
- nameWidth - 2){
|
||||
dotsAndNumber = "." + dotsAndNumber;
|
||||
}
|
||||
|
||||
GuiWizardHandbook.this.fontRenderer.drawString(dotsAndNumber, x + GuiWizardHandbook.PAGE_WIDTH
|
||||
- GuiWizardHandbook.this.fontRenderer.getStringWidth(dotsAndNumber), y, BLACK);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onGuiClosed(){
|
||||
super.onGuiClosed();
|
||||
Keyboard.enableRepeatEvents(false);
|
||||
}
|
||||
|
||||
/**
|
||||
* Fired when a control is clicked. This is the equivalent of ActionListener.actionPerformed(ActionEvent e).
|
||||
*/
|
||||
@Override
|
||||
protected void actionPerformed(GuiButton par1GuiButton){
|
||||
|
||||
if(par1GuiButton.enabled){
|
||||
if(par1GuiButton.id == 0){
|
||||
if(pageNumber < (text.size() - 1) / 2) pageNumber++;
|
||||
}else if(par1GuiButton.id == 1){
|
||||
if(pageNumber > 0) pageNumber--;
|
||||
}else{
|
||||
if(pageNumber == 1) pageNumber = (sections.get(par1GuiButton.id - 2).pageNumber - 1) / 2;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Args: left, top, width, height, pointX, pointY. Note: left, top are local to Gui, pointX, pointY are local to
|
||||
* screen
|
||||
*/
|
||||
protected boolean isPointInRegion(int par1, int par2, int par3, int par4, int par5, int par6){
|
||||
int k1 = this.width / 2 - xSize / 2;
|
||||
int l1 = this.height / 2 - this.ySize / 2;
|
||||
par5 -= k1;
|
||||
par6 -= l1;
|
||||
return par5 >= par1 - 1 && par5 < par1 + par3 + 1 && par6 >= par2 - 1 && par6 < par2 + par4 + 1;
|
||||
}
|
||||
|
||||
/**
|
||||
* Draws a textured rectangle, stretching the section of the image to fit the size given.
|
||||
*
|
||||
* @param x The x position of the rectangle
|
||||
* @param y The y position of the rectangle
|
||||
* @param u The x position of the top left corner of the section of the image wanted, expressed as a fraction of the
|
||||
* image width
|
||||
* @param v The y position of the top left corner of the section of the image wanted, expressed as a fraction of the
|
||||
* image width
|
||||
* @param finalWidth The width as rendered
|
||||
* @param finalHeight The height as rendered
|
||||
* @param width The width of the section, expressed as a fraction of the image width
|
||||
* @param height The height of the section, expressed as a fraction of the image width
|
||||
*/
|
||||
public static void drawTexturedStretchedRect(int x, int y, int u, int v, int finalWidth, int finalHeight, int width,
|
||||
int height){
|
||||
|
||||
Tessellator tessellator = Tessellator.getInstance();
|
||||
BufferBuilder buffer = tessellator.getBuffer();
|
||||
buffer.begin(GL11.GL_QUADS, DefaultVertexFormats.POSITION_TEX);
|
||||
buffer.pos((x), y + finalHeight, 0).tex(u, v + height).endVertex();
|
||||
buffer.pos(x + finalWidth, y + finalHeight, 0).tex(u + width, v + height).endVertex();
|
||||
buffer.pos(x + finalWidth, (y), 0).tex(u + width, v).endVertex();
|
||||
buffer.pos((x), (y), 0).tex(u, v).endVertex();
|
||||
tessellator.draw();
|
||||
}
|
||||
|
||||
private static NonNullList<NonNullList<ItemStack>> createGrid(){
|
||||
NonNullList<NonNullList<ItemStack>> grid = NonNullList.withSize(3, NonNullList.create());
|
||||
for(int i=0; i<3; i++){
|
||||
grid.set(i, NonNullList.withSize(3, ItemStack.EMPTY));
|
||||
}
|
||||
return grid;
|
||||
}
|
||||
|
||||
/** Called from init() in the main mod class to initialise the recipes for display in the handbook. */
|
||||
public static void initDisplayRecipes(){
|
||||
|
||||
NonNullList<NonNullList<ItemStack>> craftingGrid;
|
||||
ItemStack craftingResult;
|
||||
|
||||
craftingGrid = createGrid();
|
||||
craftingGrid.get(0).set(0, new ItemStack(Items.GOLD_NUGGET));
|
||||
craftingGrid.get(1).set(0, new ItemStack(Blocks.CARPET, 1, 10));
|
||||
craftingGrid.get(2).set(0, new ItemStack(Items.GOLD_NUGGET));
|
||||
craftingGrid.get(0).set(1, new ItemStack(WizardryItems.magic_crystal));
|
||||
craftingGrid.get(1).set(1, new ItemStack(Blocks.LAPIS_BLOCK));
|
||||
craftingGrid.get(2).set(1, new ItemStack(WizardryItems.magic_crystal));
|
||||
craftingGrid.get(0).set(2, new ItemStack(Blocks.STONE));
|
||||
craftingGrid.get(1).set(2, new ItemStack(Blocks.STONE));
|
||||
craftingGrid.get(2).set(2, new ItemStack(Blocks.STONE));
|
||||
craftingResult = new ItemStack(WizardryBlocks.arcane_workbench);
|
||||
RECIPES.add(ImmutablePair.of(craftingResult, craftingGrid));
|
||||
|
||||
craftingGrid = createGrid();
|
||||
craftingGrid.get(2).set(0, new ItemStack(WizardryItems.magic_crystal));
|
||||
craftingGrid.get(1).set(1, new ItemStack(Items.STICK));
|
||||
craftingGrid.get(0).set(2, new ItemStack(Items.GOLD_NUGGET));
|
||||
craftingResult = new ItemStack(WizardryItems.magic_wand);
|
||||
RECIPES.add(ImmutablePair.of(craftingResult, craftingGrid));
|
||||
|
||||
craftingGrid = createGrid();
|
||||
craftingGrid.get(1).set(0, new ItemStack(WizardryItems.magic_crystal));
|
||||
craftingGrid.get(0).set(1, new ItemStack(WizardryItems.magic_crystal));
|
||||
craftingGrid.get(1).set(1, new ItemStack(Items.BOOK));
|
||||
craftingGrid.get(1).set(2, new ItemStack(WizardryItems.magic_crystal));
|
||||
craftingGrid.get(2).set(1, new ItemStack(WizardryItems.magic_crystal));
|
||||
craftingResult = new ItemStack(WizardryItems.spell_book, 1, 1);
|
||||
RECIPES.add(ImmutablePair.of(craftingResult, craftingGrid));
|
||||
|
||||
craftingGrid = createGrid();
|
||||
craftingGrid.get(0).set(0, new ItemStack(Items.BOOK));
|
||||
craftingGrid.get(1).set(0, new ItemStack(WizardryItems.magic_crystal));
|
||||
craftingResult = new ItemStack(WizardryItems.wizard_handbook);
|
||||
RECIPES.add(ImmutablePair.of(craftingResult, craftingGrid));
|
||||
|
||||
craftingGrid = createGrid();
|
||||
craftingGrid.get(0).set(0, new ItemStack(WizardryBlocks.crystal_flower));
|
||||
craftingResult = new ItemStack(WizardryItems.magic_crystal, 2);
|
||||
RECIPES.add(ImmutablePair.of(craftingResult, craftingGrid));
|
||||
|
||||
craftingGrid = createGrid();
|
||||
craftingGrid.get(0).set(0, new ItemStack(WizardryItems.magic_crystal));
|
||||
craftingGrid.get(1).set(0, new ItemStack(WizardryItems.magic_crystal));
|
||||
craftingGrid.get(2).set(0, new ItemStack(WizardryItems.magic_crystal));
|
||||
craftingGrid.get(0).set(1, new ItemStack(WizardryItems.magic_crystal));
|
||||
craftingGrid.get(1).set(1, new ItemStack(Items.GLASS_BOTTLE));
|
||||
craftingGrid.get(2).set(1, new ItemStack(WizardryItems.magic_crystal));
|
||||
craftingGrid.get(0).set(2, new ItemStack(WizardryItems.magic_crystal));
|
||||
craftingGrid.get(1).set(2, new ItemStack(WizardryItems.magic_crystal));
|
||||
craftingGrid.get(2).set(2, new ItemStack(WizardryItems.magic_crystal));
|
||||
craftingResult = new ItemStack(WizardryItems.mana_flask);
|
||||
RECIPES.add(ImmutablePair.of(craftingResult, craftingGrid));
|
||||
|
||||
craftingGrid = createGrid();
|
||||
craftingGrid.get(1).set(0, new ItemStack(Blocks.STONE));
|
||||
craftingGrid.get(0).set(1, new ItemStack(Blocks.STONE));
|
||||
craftingGrid.get(1).set(1, new ItemStack(WizardryItems.magic_crystal));
|
||||
craftingGrid.get(1).set(2, new ItemStack(Blocks.STONE));
|
||||
craftingGrid.get(2).set(1, new ItemStack(Blocks.STONE));
|
||||
craftingResult = new ItemStack(WizardryBlocks.transportation_stone, 2);
|
||||
RECIPES.add(ImmutablePair.of(craftingResult, craftingGrid));
|
||||
|
||||
craftingGrid = createGrid();
|
||||
craftingGrid.get(1).set(0, new ItemStack(Items.STRING));
|
||||
craftingGrid.get(0).set(1, new ItemStack(Items.STRING));
|
||||
craftingGrid.get(1).set(1, new ItemStack(WizardryItems.magic_crystal));
|
||||
craftingGrid.get(1).set(2, new ItemStack(Items.STRING));
|
||||
craftingGrid.get(2).set(1, new ItemStack(Items.STRING));
|
||||
craftingResult = new ItemStack(WizardryItems.magic_silk, 2);
|
||||
RECIPES.add(ImmutablePair.of(craftingResult, craftingGrid));
|
||||
|
||||
craftingGrid = createGrid();
|
||||
craftingGrid.get(0).set(0, new ItemStack(WizardryItems.magic_silk));
|
||||
craftingGrid.get(1).set(0, new ItemStack(WizardryItems.magic_silk));
|
||||
craftingGrid.get(2).set(0, new ItemStack(WizardryItems.magic_silk));
|
||||
craftingGrid.get(0).set(1, new ItemStack(WizardryItems.magic_silk));
|
||||
craftingGrid.get(2).set(1, new ItemStack(WizardryItems.magic_silk));
|
||||
craftingResult = new ItemStack(WizardryItems.wizard_hat);
|
||||
RECIPES.add(ImmutablePair.of(craftingResult, craftingGrid));
|
||||
|
||||
craftingGrid = createGrid();
|
||||
craftingGrid.get(0).set(0, new ItemStack(WizardryItems.magic_silk));
|
||||
craftingGrid.get(2).set(0, new ItemStack(WizardryItems.magic_silk));
|
||||
craftingGrid.get(0).set(1, new ItemStack(WizardryItems.magic_silk));
|
||||
craftingGrid.get(1).set(1, new ItemStack(WizardryItems.magic_silk));
|
||||
craftingGrid.get(2).set(1, new ItemStack(WizardryItems.magic_silk));
|
||||
craftingGrid.get(0).set(2, new ItemStack(WizardryItems.magic_silk));
|
||||
craftingGrid.get(1).set(2, new ItemStack(WizardryItems.magic_silk));
|
||||
craftingGrid.get(2).set(2, new ItemStack(WizardryItems.magic_silk));
|
||||
craftingResult = new ItemStack(WizardryItems.wizard_robe);
|
||||
RECIPES.add(ImmutablePair.of(craftingResult, craftingGrid));
|
||||
|
||||
craftingGrid = createGrid();
|
||||
craftingGrid.get(0).set(0, new ItemStack(WizardryItems.magic_silk));
|
||||
craftingGrid.get(1).set(0, new ItemStack(WizardryItems.magic_silk));
|
||||
craftingGrid.get(2).set(0, new ItemStack(WizardryItems.magic_silk));
|
||||
craftingGrid.get(0).set(1, new ItemStack(WizardryItems.magic_silk));
|
||||
craftingGrid.get(2).set(1, new ItemStack(WizardryItems.magic_silk));
|
||||
craftingGrid.get(0).set(2, new ItemStack(WizardryItems.magic_silk));
|
||||
craftingGrid.get(2).set(2, new ItemStack(WizardryItems.magic_silk));
|
||||
craftingResult = new ItemStack(WizardryItems.wizard_leggings);
|
||||
RECIPES.add(ImmutablePair.of(craftingResult, craftingGrid));
|
||||
|
||||
craftingGrid = createGrid();
|
||||
craftingGrid.get(0).set(0, new ItemStack(WizardryItems.magic_silk));
|
||||
craftingGrid.get(2).set(0, new ItemStack(WizardryItems.magic_silk));
|
||||
craftingGrid.get(0).set(1, new ItemStack(WizardryItems.magic_silk));
|
||||
craftingGrid.get(2).set(1, new ItemStack(WizardryItems.magic_silk));
|
||||
craftingResult = new ItemStack(WizardryItems.wizard_boots);
|
||||
RECIPES.add(ImmutablePair.of(craftingResult, craftingGrid));
|
||||
|
||||
craftingGrid = createGrid();
|
||||
craftingGrid.get(0).set(0, new ItemStack(Items.PAPER));
|
||||
craftingGrid.get(1).set(0, new ItemStack(Items.STRING));
|
||||
craftingResult = new ItemStack(WizardryItems.blank_scroll);
|
||||
RECIPES.add(ImmutablePair.of(craftingResult, craftingGrid));
|
||||
|
||||
craftingGrid = createGrid();
|
||||
craftingGrid.get(0).set(0, new ItemStack(Items.BLAZE_POWDER));
|
||||
craftingGrid.get(1).set(0, new ItemStack(Items.BLAZE_POWDER));
|
||||
craftingGrid.get(0).set(1, new ItemStack(Items.GLASS_BOTTLE));
|
||||
craftingGrid.get(1).set(1, new ItemStack(Items.GUNPOWDER));
|
||||
craftingResult = new ItemStack(WizardryItems.firebomb, 3);
|
||||
RECIPES.add(ImmutablePair.of(craftingResult, craftingGrid));
|
||||
|
||||
craftingGrid = createGrid();
|
||||
craftingGrid.get(0).set(0, new ItemStack(Items.SPIDER_EYE));
|
||||
craftingGrid.get(1).set(0, new ItemStack(Items.SPIDER_EYE));
|
||||
craftingGrid.get(0).set(1, new ItemStack(Items.GLASS_BOTTLE));
|
||||
craftingGrid.get(1).set(1, new ItemStack(Items.GUNPOWDER));
|
||||
craftingResult = new ItemStack(WizardryItems.poison_bomb, 3);
|
||||
RECIPES.add(ImmutablePair.of(craftingResult, craftingGrid));
|
||||
|
||||
craftingGrid = createGrid();
|
||||
craftingGrid.get(0).set(0, new ItemStack(Items.COAL));
|
||||
craftingGrid.get(1).set(0, new ItemStack(Items.COAL));
|
||||
craftingGrid.get(0).set(1, new ItemStack(Items.GLASS_BOTTLE));
|
||||
craftingGrid.get(1).set(1, new ItemStack(Items.GUNPOWDER));
|
||||
craftingResult = new ItemStack(WizardryItems.smoke_bomb, 3);
|
||||
RECIPES.add(ImmutablePair.of(craftingResult, craftingGrid));
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,181 @@
|
||||
package electroblob.wizardry.client.gui.handbook;
|
||||
|
||||
import com.google.gson.JsonObject;
|
||||
import com.google.gson.JsonSyntaxException;
|
||||
import electroblob.wizardry.client.DrawingUtils;
|
||||
import electroblob.wizardry.util.WizardryUtilities;
|
||||
import net.minecraft.client.gui.FontRenderer;
|
||||
import net.minecraft.client.gui.GuiButton;
|
||||
import net.minecraft.util.JsonUtils;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collection;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* Instances of this class represent tables of contents in the wizard's handbook. Each {@link Section} can have a
|
||||
* single table of contents, which can reference any other sections in the handbook (though it is normal to list
|
||||
* top-level sections in a main contents and have subsections listed in their respective parent sections' contents).
|
||||
*
|
||||
* This class handles JSON parsing, formatting and drawing of the contents itself, working on a line-by-line basis
|
||||
* (as opposed to sections, which work on a page-by-page basis). It also stores its own list of buttons.
|
||||
*
|
||||
* @author Electroblob
|
||||
* @since Wizardry 4.2
|
||||
*/
|
||||
class Contents {
|
||||
|
||||
// Final fields are mandatory, the rest are optional
|
||||
final String id;
|
||||
private boolean hyperlinks = true;
|
||||
private boolean pageNumbers = true;
|
||||
private String separator = ".";
|
||||
// Derived fields, not specifically defined in JSON
|
||||
private int startPage;
|
||||
private int startLine;
|
||||
private final List<List<GuiButton>> buttons;
|
||||
|
||||
private final List<Section> entries;
|
||||
|
||||
private Contents(String id){
|
||||
this.id = id;
|
||||
this.entries = new ArrayList<>();
|
||||
this.buttons = new ArrayList<>();
|
||||
}
|
||||
|
||||
/** Returns an unmodifiable, flattened collection of all the buttons in this contents. */
|
||||
Collection<GuiButton> getButtons(){
|
||||
return WizardryUtilities.flatten(buttons);
|
||||
}
|
||||
|
||||
void addEntry(Section section){
|
||||
entries.add(section);
|
||||
}
|
||||
|
||||
/**
|
||||
* Draws this contents for the given double-page spread and shows/hides buttons accordingly. Will draw nothing
|
||||
* if the given page is outside of this contents.
|
||||
*
|
||||
* @param font The font renderer object.
|
||||
* @param doublePage The index of the <b>double-page</b> to be drawn.
|
||||
* @param left The x coordinate of the left side of the GUI.
|
||||
* @param top The y coordinate of the top of the GUI.
|
||||
*/
|
||||
void draw(FontRenderer font, int doublePage, int left, int top){
|
||||
|
||||
// Show/hide buttons
|
||||
|
||||
int i = 0;
|
||||
|
||||
for(List<GuiButton> list : buttons){
|
||||
final int i1 = i++;
|
||||
list.forEach(b -> b.visible = GuiWizardHandbook.singleToDoublePage(startPage + i1) == doublePage);
|
||||
}
|
||||
|
||||
if(!pageNumbers) return; // No page numbers means only the buttons are drawn
|
||||
|
||||
// FONT_HEIGHT may change between fonts, so this is calculated here. With the default font it's 14.
|
||||
final int maxLineNumber = GuiWizardHandbook.PAGE_HEIGHT / font.FONT_HEIGHT;
|
||||
|
||||
int leftIndex = GuiWizardHandbook.doubleToSinglePage(doublePage, false);
|
||||
// Relative indices of the pages to be rendered - often these will be outside the section entirely
|
||||
int[] visiblePages = {leftIndex - startPage, leftIndex - startPage + 1};
|
||||
|
||||
for(int page : visiblePages){
|
||||
|
||||
if(page >= 0 && page < entries.size() / maxLineNumber + 1){
|
||||
|
||||
int x = left + (GuiWizardHandbook.isRightPage(startPage + page) ? GuiWizardHandbook.GUI_WIDTH - GuiWizardHandbook.TEXT_INSET_X - GuiWizardHandbook.PAGE_WIDTH : GuiWizardHandbook.TEXT_INSET_X);
|
||||
int y = top + GuiWizardHandbook.TEXT_INSET_Y + startLine * font.FONT_HEIGHT;
|
||||
|
||||
for(Section entry : this.entries){
|
||||
|
||||
int nameWidth = font.getStringWidth(entry.title);
|
||||
|
||||
String dotsAndNumber = " " + entry.startPage;
|
||||
|
||||
while(font.getStringWidth(dotsAndNumber) < GuiWizardHandbook.PAGE_WIDTH - nameWidth - 2){
|
||||
dotsAndNumber = separator + dotsAndNumber;
|
||||
}
|
||||
|
||||
font.drawString(dotsAndNumber, x + GuiWizardHandbook.PAGE_WIDTH - font.getStringWidth(dotsAndNumber), y, DrawingUtils.BLACK, false);
|
||||
|
||||
if(!hyperlinks) font.drawString(entry.title, x, y, DrawingUtils.BLACK, false);
|
||||
|
||||
y += font.FONT_HEIGHT;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Called on GUI load to format the section and all subsections, contents tables and other elements. Does not
|
||||
* perform any actual drawing.
|
||||
*
|
||||
* @param font The font renderer object, for measurement purposes.
|
||||
* @param startPage The index of the first page (single side, not double-page) of this section.
|
||||
* @param startLine The index of the first line of this contents.
|
||||
* @param left The x coordinate of the left side of the GUI.
|
||||
* @param top The y coordinate of the top of the GUI.
|
||||
* @return The number of lines this contents takes up.
|
||||
* @throws JsonSyntaxException if at any point the formatting is found to be invalid.
|
||||
*/
|
||||
int format(FontRenderer font, int startPage, int startLine, int left, int top){
|
||||
|
||||
this.buttons.clear();
|
||||
|
||||
if(hyperlinks){
|
||||
|
||||
// FONT_HEIGHT may change between fonts, so this is calculated here. With the default font it's 14.
|
||||
final int maxLineNumber = GuiWizardHandbook.PAGE_HEIGHT / font.FONT_HEIGHT;
|
||||
|
||||
this.startPage = startPage;
|
||||
this.startLine = startLine;
|
||||
|
||||
List<GuiButton> list = new ArrayList<>(maxLineNumber);
|
||||
|
||||
for(Section entry : this.entries){
|
||||
|
||||
int x = GuiWizardHandbook.isRightPage(startPage) ? left + GuiWizardHandbook.GUI_WIDTH - GuiWizardHandbook.TEXT_INSET_X - GuiWizardHandbook.PAGE_WIDTH : left + GuiWizardHandbook.TEXT_INSET_X;
|
||||
int y = top + GuiWizardHandbook.TEXT_INSET_Y + startLine * font.FONT_HEIGHT;
|
||||
|
||||
list.add(new GuiButtonHyperlink.Internal(0, x, y, font, entry.title, entry, 0, ""));
|
||||
|
||||
startLine++;
|
||||
|
||||
if(startLine == maxLineNumber){
|
||||
startLine = 0;
|
||||
startPage++;
|
||||
buttons.add(list);
|
||||
list = new ArrayList<>(maxLineNumber); // If there are no more entries this will be discarded anyway
|
||||
}
|
||||
}
|
||||
|
||||
buttons.add(list);
|
||||
}
|
||||
|
||||
// Returning this is kind of trivial at the moment but if we ever wanted to add a header or something,
|
||||
// it would be more useful.
|
||||
return entries.size();
|
||||
}
|
||||
|
||||
/**
|
||||
* Parses the given JSON object and constructs a new {@code Contents} from it, setting all the relevant fields
|
||||
* and references.
|
||||
*
|
||||
* @param json A JSON object representing the contents to be constructed. This must contain at least an "id"
|
||||
* string.
|
||||
* @return The resulting {@code Contents} object.
|
||||
* @throws JsonSyntaxException if at any point the JSON object is found to be invalid.
|
||||
*/
|
||||
static Contents fromJson(JsonObject json){
|
||||
|
||||
Contents contents = new Contents(JsonUtils.getString(json, "id"));
|
||||
|
||||
contents.hyperlinks = JsonUtils.getBoolean(json, "hyperlinks", true);
|
||||
contents.pageNumbers = JsonUtils.getBoolean(json, "page_numbers", true);
|
||||
contents.separator = JsonUtils.getString(json, "separator", ".");
|
||||
|
||||
return contents;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,225 @@
|
||||
package electroblob.wizardry.client.gui.handbook;
|
||||
|
||||
import com.google.gson.JsonElement;
|
||||
import com.google.gson.JsonObject;
|
||||
import com.google.gson.JsonSyntaxException;
|
||||
import electroblob.wizardry.client.DrawingUtils;
|
||||
import electroblob.wizardry.spell.Mine;
|
||||
import net.minecraft.client.Minecraft;
|
||||
import net.minecraft.client.gui.FontRenderer;
|
||||
import net.minecraft.client.gui.GuiScreen;
|
||||
import net.minecraft.client.gui.recipebook.GuiRecipeBook;
|
||||
import net.minecraft.client.renderer.GlStateManager;
|
||||
import net.minecraft.client.renderer.RenderHelper;
|
||||
import net.minecraft.client.renderer.RenderItem;
|
||||
import net.minecraft.item.Item;
|
||||
import net.minecraft.item.ItemStack;
|
||||
import net.minecraft.item.crafting.CraftingManager;
|
||||
import net.minecraft.item.crafting.IRecipe;
|
||||
import net.minecraft.item.crafting.Ingredient;
|
||||
import net.minecraft.stats.RecipeBook;
|
||||
import net.minecraft.util.JsonUtils;
|
||||
import net.minecraft.util.NonNullList;
|
||||
import net.minecraft.util.ResourceLocation;
|
||||
|
||||
import java.util.HashSet;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
|
||||
class CraftingRecipe {
|
||||
|
||||
static final int WIDTH = 111, HEIGHT = 56;
|
||||
|
||||
// Final fields are mandatory, the rest are optional
|
||||
private final ResourceLocation location;
|
||||
// Derived fields, not specifically defined in JSON
|
||||
private IRecipe recipe;
|
||||
private final Set<int[]> instances = new HashSet<>();
|
||||
|
||||
private CraftingRecipe(ResourceLocation location){
|
||||
this.location = location;
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds an instance of this recipe to the list.
|
||||
*
|
||||
* @param page The index of the <b>single</b> page this image is on.
|
||||
* @param x The x-coordinate of the top-left corner of the image, <i>relative</i> to the top-left corner of the GUI.
|
||||
* @param y The y-coordinate of the top-left corner of the image, <i>relative</i> to the top-left corner of the GUI.
|
||||
*/
|
||||
void addInstance(int page, int x, int y){
|
||||
instances.add(new int[]{page, x, y});
|
||||
}
|
||||
|
||||
/** Removes all instances of this recipe from the list. */
|
||||
void clearInstances(){
|
||||
instances.clear();
|
||||
}
|
||||
|
||||
/** Called on GUI open to load the actual recipe object from the registry. This cannot be done on JSON load since
|
||||
* the recipes aren't necessarily loaded at that point. */
|
||||
void load(){
|
||||
|
||||
this.recipe = CraftingManager.getRecipe(location);
|
||||
|
||||
if(recipe == null) throw new JsonSyntaxException("No such recipe: " + location);
|
||||
}
|
||||
|
||||
/**
|
||||
* Draws all instances of this recipe that are located on the given double-page spread.
|
||||
*
|
||||
* @param font The font renderer object.
|
||||
* @param itemRenderer The item renderer object.
|
||||
* @param doublePage The double-page index of the page to be drawn.
|
||||
* @param left The x coordinate of the left side of the GUI.
|
||||
* @param top The y coordinate of the top of the GUI.
|
||||
*/
|
||||
void draw(FontRenderer font, RenderItem itemRenderer, int doublePage, int left, int top){
|
||||
for(int[] instance : instances){
|
||||
if(GuiWizardHandbook.singleToDoublePage(instance[0]) == doublePage){
|
||||
renderCraftingRecipe(font, itemRenderer, left + instance[1], top + instance[2], this.recipe);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Draws the tooltips for all instances of this recipe that are located on the given double-page spread. This has to
|
||||
* be done separately so that the tooltips are on top of everything else.
|
||||
*
|
||||
* @param itemRenderer The item renderer object.
|
||||
* @param doublePage The double-page index of the page to be drawn.
|
||||
* @param left The x coordinate of the left side of the GUI.
|
||||
* @param top The y coordinate of the top of the GUI.
|
||||
*/
|
||||
void drawTooltips(GuiWizardHandbook gui, FontRenderer font, RenderItem itemRenderer, int doublePage, int left, int top, int mouseX, int mouseY){
|
||||
for(int[] instance : instances){
|
||||
if(GuiWizardHandbook.singleToDoublePage(instance[0]) == doublePage){
|
||||
renderCraftingTooltips(gui, itemRenderer, left + instance[1], top + instance[2], mouseX, mouseY, this.recipe);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Parses the given JSON object and constructs a new {@code Image} from it, setting all the relevant fields
|
||||
* and references.
|
||||
*
|
||||
* @param json A JSON object representing the image to be constructed. This must contain at least a "location"
|
||||
* string.
|
||||
* @return The resulting {@code Image} object.
|
||||
* @throws JsonSyntaxException if at any point the JSON object is found to be invalid.
|
||||
*/
|
||||
static CraftingRecipe fromJson(JsonObject json){
|
||||
|
||||
ResourceLocation location = new ResourceLocation(JsonUtils.getString(json, "location"));
|
||||
return new CraftingRecipe(location);
|
||||
}
|
||||
|
||||
static void populate(Map<String, CraftingRecipe> map, JsonObject json){
|
||||
|
||||
JsonObject sectionsObject = JsonUtils.getJsonObject(json, "recipes");
|
||||
|
||||
// Need to iterate over these since we don't know what they're called or how many there are
|
||||
for(Map.Entry<String, JsonElement> entry : sectionsObject.entrySet()){
|
||||
|
||||
String key = entry.getKey(); // Find out what each element is called, this will be the sections map key
|
||||
|
||||
CraftingRecipe recipe = fromJson(entry.getValue().getAsJsonObject());
|
||||
map.put(key, recipe);
|
||||
}
|
||||
}
|
||||
|
||||
private static void renderCraftingRecipe(FontRenderer font, RenderItem itemRenderer, int x, int y, IRecipe recipe){
|
||||
|
||||
ItemStack result = recipe.getRecipeOutput();
|
||||
|
||||
GlStateManager.color(1, 1, 1, 1);
|
||||
Minecraft.getMinecraft().renderEngine.bindTexture(GuiWizardHandbook.texture);
|
||||
|
||||
DrawingUtils.drawTexturedRect(x - 2, y - 2, 60, 190, WIDTH, HEIGHT, 512, 256);
|
||||
|
||||
GlStateManager.pushMatrix();
|
||||
RenderHelper.enableGUIStandardItemLighting();
|
||||
GlStateManager.disableLighting();
|
||||
GlStateManager.enableRescaleNormal();
|
||||
GlStateManager.enableColorMaterial();
|
||||
GlStateManager.enableLighting();
|
||||
itemRenderer.zLevel = 100.0F;
|
||||
|
||||
// TODO: There may be a better way of doing this
|
||||
int index = (int)(Minecraft.getSystemTime() % Integer.MAX_VALUE)/2000;
|
||||
|
||||
int i = 0;
|
||||
|
||||
for(Ingredient ingredient : recipe.getIngredients()){
|
||||
|
||||
if(ingredient != Ingredient.EMPTY){
|
||||
ItemStack stack = ingredient.getMatchingStacks()[index % ingredient.getMatchingStacks().length];
|
||||
if(!stack.isEmpty()){
|
||||
itemRenderer.renderItemAndEffectIntoGUI(stack, x + 18 * (i%3), y + 18 * (i/3));
|
||||
itemRenderer.renderItemOverlays(font, stack, x + 18 * (i%3), y + 18 * (i/3));
|
||||
}
|
||||
}
|
||||
|
||||
i++;
|
||||
}
|
||||
|
||||
if(!result.isEmpty()){
|
||||
itemRenderer.renderItemAndEffectIntoGUI(result, x + 86, y + 18);
|
||||
itemRenderer.renderItemOverlays(font, result, x + 86, y + 18);
|
||||
}
|
||||
|
||||
GlStateManager.popMatrix();
|
||||
GlStateManager.enableLighting();
|
||||
GlStateManager.enableDepth();
|
||||
GlStateManager.disableColorMaterial();
|
||||
itemRenderer.zLevel = 0.0F;
|
||||
//RenderHelper.enableStandardItemLighting();
|
||||
|
||||
}
|
||||
|
||||
private static void renderCraftingTooltips(GuiWizardHandbook gui, RenderItem itemRenderer, int x, int y, int mouseX, int mouseY, IRecipe recipe){
|
||||
|
||||
ItemStack result = recipe.getRecipeOutput();
|
||||
|
||||
GlStateManager.pushMatrix();
|
||||
RenderHelper.enableGUIStandardItemLighting();
|
||||
GlStateManager.disableLighting();
|
||||
GlStateManager.enableRescaleNormal();
|
||||
GlStateManager.enableColorMaterial();
|
||||
itemRenderer.zLevel = 0.0F;
|
||||
GlStateManager.disableLighting();
|
||||
|
||||
// TODO: There may be a better way of doing this
|
||||
int index = (int)(Minecraft.getSystemTime() % Integer.MAX_VALUE)/2000;
|
||||
|
||||
int i = 0;
|
||||
|
||||
for(Ingredient ingredient : recipe.getIngredients()){
|
||||
|
||||
if(ingredient != Ingredient.EMPTY){
|
||||
ItemStack stack = ingredient.getMatchingStacks()[index % ingredient.getMatchingStacks().length];
|
||||
if(!stack.isEmpty() && isPointInRegion(x + 18 * (i%3), y + 18 * (i/3), 16, 16, mouseX, mouseY)){
|
||||
gui.renderToolTip(stack, mouseX, mouseY);
|
||||
}
|
||||
}
|
||||
|
||||
i++;
|
||||
}
|
||||
|
||||
if(!result.isEmpty() && isPointInRegion(x + 86, y + 18, 16, 16, mouseX, mouseY)){
|
||||
gui.renderToolTip(result, mouseX, mouseY);
|
||||
}
|
||||
|
||||
GlStateManager.popMatrix();
|
||||
GlStateManager.enableLighting();
|
||||
GlStateManager.enableDepth();
|
||||
GlStateManager.disableColorMaterial();
|
||||
RenderHelper.enableStandardItemLighting();
|
||||
|
||||
}
|
||||
|
||||
private static boolean isPointInRegion(int left, int top, int width, int height, int mouseX, int mouseY){
|
||||
return mouseX >= left - 1 && mouseX < left + width + 1 && mouseY >= top - 1 && mouseY < top + height + 1;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,179 @@
|
||||
package electroblob.wizardry.client.gui.handbook;
|
||||
|
||||
import com.google.gson.JsonSyntaxException;
|
||||
import electroblob.wizardry.registry.WizardrySounds;
|
||||
import net.minecraft.client.Minecraft;
|
||||
import net.minecraft.client.audio.PositionedSoundRecord;
|
||||
import net.minecraft.client.audio.SoundHandler;
|
||||
import net.minecraft.client.gui.FontRenderer;
|
||||
import net.minecraft.client.gui.GuiButton;
|
||||
import net.minecraft.util.text.ITextComponent;
|
||||
import net.minecraft.util.text.TextComponentString;
|
||||
import net.minecraft.util.text.TextFormatting;
|
||||
import net.minecraft.util.text.event.ClickEvent;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
public abstract class GuiButtonHyperlink extends GuiButton {
|
||||
|
||||
public static final String URL_REGEX = "^((https?|ftp)://|(www|ftp)\\.)?[a-z0-9-]+(\\.[a-z0-9-]+)+([/?].*)?$";
|
||||
|
||||
final int indent;
|
||||
final List<String> lines;
|
||||
|
||||
GuiButtonHyperlink(int id, int x, int y, FontRenderer font, String text, int indent, String suffix){
|
||||
|
||||
super(id, x, y, font.getStringWidth(text), font.FONT_HEIGHT, text);
|
||||
|
||||
// Sometimes a link has punctuation or something after it that causes it to wrap onto a new line
|
||||
String linkWithSuffix = text + suffix;
|
||||
|
||||
// If the string won't fit any words at the end of the current line, treat it as if we started a new line
|
||||
if(font.getStringWidth(linkWithSuffix.split("\\s")[0]) > GuiWizardHandbook.PAGE_WIDTH - indent){
|
||||
indent = 0;
|
||||
this.y += font.FONT_HEIGHT;
|
||||
}
|
||||
|
||||
this.indent = indent; // Assigned here in case it was corrected above
|
||||
|
||||
String line1 = font.listFormattedStringToWidth(linkWithSuffix, GuiWizardHandbook.PAGE_WIDTH - indent).get(0);
|
||||
// Without trim(), there will be at least 1 leading space due to the custom wrapping
|
||||
String remainder = linkWithSuffix.substring(line1.length()).trim();
|
||||
|
||||
// ... then wrap the rest to the normal width.
|
||||
lines = new ArrayList<>();
|
||||
lines.add(line1);
|
||||
// Some links are only one line, if this wasn't checked they would cause a StackOverflowError
|
||||
if(!remainder.isEmpty()) lines.addAll(font.listFormattedStringToWidth(remainder, GuiWizardHandbook.PAGE_WIDTH));
|
||||
|
||||
// Removes the suffix if it exists (ugly as heck, but it works)
|
||||
if(!suffix.isEmpty()){
|
||||
for(int i=lines.size()-1; i>=0; i--){
|
||||
String line = lines.get(i);
|
||||
if(suffix.endsWith(line)){
|
||||
lines.remove(i);
|
||||
}else if(line.endsWith(suffix)){
|
||||
lines.set(i, line.substring(0, line.length() - suffix.length()));
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public boolean isHovered(net.minecraft.client.gui.FontRenderer font, int mouseX, int mouseY){
|
||||
|
||||
int i = 0;
|
||||
|
||||
for(String line : lines){
|
||||
|
||||
int l = x;
|
||||
if(i == 0) l += indent;
|
||||
|
||||
int t = y + font.FONT_HEIGHT * i;
|
||||
|
||||
if(mouseX >= l && mouseY >= t && mouseX < l + font.getStringWidth(line) && mouseY < t + font.FONT_HEIGHT){
|
||||
return true;
|
||||
}
|
||||
|
||||
i++;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean mousePressed(Minecraft minecraft, int mouseX, int mouseY){
|
||||
return this.enabled && this.visible && isHovered(minecraft.fontRenderer, mouseX, mouseY);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void drawButton(Minecraft minecraft, int mouseX, int mouseY, float partialTicks){
|
||||
|
||||
if(this.visible){
|
||||
|
||||
this.hovered = isHovered(minecraft.fontRenderer, mouseX, mouseY);
|
||||
int colour = hovered ? GuiWizardHandbook.colours.get("highlight") : GuiWizardHandbook.colours.get("hyperlink");
|
||||
|
||||
int i = 0;
|
||||
|
||||
for(String line : lines){
|
||||
|
||||
int l = x;
|
||||
if(i == 0) l += indent;
|
||||
|
||||
int t = y + minecraft.fontRenderer.FONT_HEIGHT * i;
|
||||
|
||||
minecraft.fontRenderer.drawString(line, l, t, colour);
|
||||
|
||||
i++;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a new hyperlink button from the given arguments, automatically differentiating between URLs and sections.
|
||||
* @param x The x position of the button
|
||||
* @param y The y position of the button
|
||||
* @param font A reference to the FontRenderer object
|
||||
* @param upToLink The paragraph (as a list of lines) up to the link, used to determine positioning and word wrap
|
||||
* @param arguments The link arguments - that is, everything between the two @ signs, split by spaces
|
||||
* @param suffix The text directly after the link, up to the first whitespace; used for word wrap. Usually this is
|
||||
* either empty or contains a single punctuation mark.
|
||||
* @return The resulting button
|
||||
* @throws IllegalArgumentException if the given argument array is empty or contains more than 2 arguments
|
||||
* @throws JsonSyntaxException if the specified link target is not a URL or a valid section ID
|
||||
*/
|
||||
public static GuiButtonHyperlink create(int x, int y, FontRenderer font, List<String> upToLink, String[] arguments, String suffix){
|
||||
|
||||
if(arguments.length == 0 || arguments.length > 2) throw new IllegalArgumentException("Incorrect array length!");
|
||||
|
||||
GuiButtonHyperlink button;
|
||||
|
||||
if(arguments[0].matches(URL_REGEX)){
|
||||
|
||||
button = new GuiButtonHyperlink.External(0, x, y, font, arguments[arguments.length - 1], arguments[0],
|
||||
font.getStringWidth(upToLink.get(upToLink.size() - 1)), suffix);
|
||||
|
||||
}else{
|
||||
|
||||
Section target = GuiWizardHandbook.sections.get(arguments[0]);
|
||||
|
||||
if(target == null) throw new JsonSyntaxException("Hyperlink points to nonexistent section id " + arguments[0]);
|
||||
|
||||
button = new GuiButtonHyperlink.Internal(0, x, y, font, arguments[arguments.length - 1],
|
||||
target, font.getStringWidth(upToLink.get(upToLink.size() - 1)), suffix);
|
||||
}
|
||||
|
||||
return button;
|
||||
}
|
||||
|
||||
static class Internal extends GuiButtonHyperlink {
|
||||
|
||||
final Section target;
|
||||
|
||||
Internal(int id, int x, int y, FontRenderer font, String text, Section target, int indent, String suffix){
|
||||
super(id, x, y, font, text, indent, suffix);
|
||||
this.target = target;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void playPressSound(SoundHandler soundHandler){
|
||||
soundHandler.playSound(PositionedSoundRecord.getMasterRecord(WizardrySounds.MISC_PAGE_TURN, 1));
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
static class External extends GuiButtonHyperlink {
|
||||
|
||||
final ITextComponent link;
|
||||
|
||||
External(int id, int x, int y, FontRenderer font, String text, String url, int indent, String suffix){
|
||||
super(id, x, y, font, text, indent, suffix);
|
||||
this.link = new TextComponentString(text);
|
||||
link.getStyle().setClickEvent(new ClickEvent(ClickEvent.Action.OPEN_URL, url));
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,63 @@
|
||||
package electroblob.wizardry.client.gui.handbook;
|
||||
|
||||
import electroblob.wizardry.Wizardry;
|
||||
import electroblob.wizardry.client.DrawingUtils;
|
||||
import electroblob.wizardry.registry.WizardrySounds;
|
||||
import net.minecraft.client.Minecraft;
|
||||
import net.minecraft.client.audio.PositionedSoundRecord;
|
||||
import net.minecraft.client.audio.SoundHandler;
|
||||
import net.minecraft.client.gui.GuiButton;
|
||||
import net.minecraft.client.renderer.GlStateManager;
|
||||
import net.minecraft.util.ResourceLocation;
|
||||
import net.minecraftforge.fml.relauncher.Side;
|
||||
import net.minecraftforge.fml.relauncher.SideOnly;
|
||||
|
||||
@SideOnly(Side.CLIENT)
|
||||
class GuiButtonTurnPage extends GuiButton {
|
||||
|
||||
static final int WIDTH = 20;
|
||||
static final int HEIGHT = 12;
|
||||
|
||||
enum Type {
|
||||
|
||||
NEXT_PAGE(0, 196),
|
||||
PREVIOUS_PAGE(0, 208),
|
||||
NEXT_SECTION(0, 220),
|
||||
PREVIOUS_SECTION(0, 232),
|
||||
CONTENTS(0, 244);
|
||||
|
||||
private final int u, v;
|
||||
|
||||
Type(int u, int v){
|
||||
this.u = u;
|
||||
this.v = v;
|
||||
}
|
||||
}
|
||||
|
||||
public final Type type;
|
||||
|
||||
private static final ResourceLocation texture = new ResourceLocation(Wizardry.MODID, "textures/gui/handbook.png");
|
||||
|
||||
public GuiButtonTurnPage(int id, int x, int y, Type type){
|
||||
super(id, x, y, WIDTH, HEIGHT, "");
|
||||
this.type = type;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void playPressSound(SoundHandler soundHandler){
|
||||
soundHandler.playSound(PositionedSoundRecord.getMasterRecord(WizardrySounds.MISC_PAGE_TURN, 1));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void drawButton(Minecraft minecraft, int mouseX, int mouseY, float partialTicks){
|
||||
|
||||
if(this.visible){
|
||||
|
||||
boolean flag = mouseX >= this.x && mouseY >= this.y && mouseX < this.x + this.width && mouseY < this.y + this.height;
|
||||
GlStateManager.color(1.0F, 1.0F, 1.0F, 1.0F);
|
||||
minecraft.getTextureManager().bindTexture(texture);
|
||||
|
||||
DrawingUtils.drawTexturedRect(this.x, this.y, flag ? type.u + width : type.u, type.v, width, height, 512, 256);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,566 @@
|
||||
package electroblob.wizardry.client.gui.handbook;
|
||||
|
||||
import com.google.gson.Gson;
|
||||
import com.google.gson.JsonElement;
|
||||
import com.google.gson.JsonObject;
|
||||
import electroblob.wizardry.Wizardry;
|
||||
import electroblob.wizardry.client.ClientProxy;
|
||||
import electroblob.wizardry.client.DrawingUtils;
|
||||
import electroblob.wizardry.client.gui.GuiButtonInvisible;
|
||||
import electroblob.wizardry.client.gui.handbook.GuiButtonTurnPage.Type;
|
||||
import electroblob.wizardry.constants.Constants;
|
||||
import electroblob.wizardry.constants.Element;
|
||||
import electroblob.wizardry.constants.Tier;
|
||||
import electroblob.wizardry.registry.WizardrySounds;
|
||||
import net.minecraft.client.Minecraft;
|
||||
import net.minecraft.client.audio.PositionedSoundRecord;
|
||||
import net.minecraft.client.audio.SoundHandler;
|
||||
import net.minecraft.client.gui.GuiButton;
|
||||
import net.minecraft.client.gui.GuiScreen;
|
||||
import net.minecraft.client.multiplayer.ClientAdvancementManager;
|
||||
import net.minecraft.client.renderer.GlStateManager;
|
||||
import net.minecraft.client.resources.IResource;
|
||||
import net.minecraft.client.resources.IResourceManager;
|
||||
import net.minecraft.item.ItemStack;
|
||||
import net.minecraft.util.JsonUtils;
|
||||
import net.minecraft.util.NonNullList;
|
||||
import net.minecraft.util.ResourceLocation;
|
||||
import net.minecraftforge.event.entity.player.AdvancementEvent;
|
||||
import net.minecraftforge.fml.common.Mod;
|
||||
import net.minecraftforge.fml.common.eventhandler.SubscribeEvent;
|
||||
import net.minecraftforge.fml.relauncher.ReflectionHelper;
|
||||
import org.apache.commons.lang3.tuple.Pair;
|
||||
import org.lwjgl.input.Keyboard;
|
||||
|
||||
import java.awt.*;
|
||||
import java.io.BufferedReader;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStreamReader;
|
||||
import java.lang.reflect.Field;
|
||||
import java.util.*;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* GUI class for the wizard's handbook. Like any GUI class, this is instantiated each time the book is opened. As of
|
||||
* Wizardry 4.2, the handbook text is defined as a JSON file rather than a plain text file, and is loaded only on
|
||||
* resource pack reload, rather than every time the book is opened. This means all the data structures (sections, images,
|
||||
* etc.) are built before the GUI instance exists at all. However, since some things depend on positioning, these have to
|
||||
* be initialised on GUI creation. (Previously, everything was done on GUI load)
|
||||
*
|
||||
* @author Electroblob
|
||||
* @since Wizardry 1.0
|
||||
* @see Section
|
||||
* @see Contents
|
||||
* @see Image
|
||||
* @see CraftingRecipe
|
||||
*/
|
||||
@Mod.EventBusSubscriber
|
||||
public class GuiWizardHandbook extends GuiScreen {
|
||||
|
||||
private static final ResourceLocation DEFAULT = new ResourceLocation(Wizardry.MODID, "texts/handbook_en_us.json");
|
||||
|
||||
static final ResourceLocation texture = new ResourceLocation(Wizardry.MODID, "textures/gui/handbook.png");
|
||||
|
||||
/** Global Gson instance for the handbook. */
|
||||
private static final Gson gson = new Gson();
|
||||
|
||||
// Formatting markup
|
||||
|
||||
static final char FORMAT_MARKER = '#';
|
||||
static final char HYPERLINK_MARKER = '@';
|
||||
|
||||
static final String IMAGE_TAG = "image";
|
||||
static final String RECIPE_TAG = "recipe";
|
||||
|
||||
static final Map<String, String> FORMAT_TAGS = new HashMap<>();
|
||||
|
||||
// Dimension constants
|
||||
// Private constants are not relevant to book elements, package-protected ones are
|
||||
|
||||
/** The dimensions of the rendered GUI area. */
|
||||
static final int GUI_WIDTH = 288, GUI_HEIGHT = 180;
|
||||
/** The dimensions of the area of a single page in which text can be drawn. */
|
||||
static final int PAGE_WIDTH = 120, PAGE_HEIGHT = 140;
|
||||
/** The distance of the text from the top outside corner of each page. */
|
||||
static final int TEXT_INSET_X = 17, TEXT_INSET_Y = 16;
|
||||
/** The distance of the buttons from the bottom outside corners of the GUI. */
|
||||
private static final int BUTTON_INSET_X = 22, BUTTON_INSET_Y = 13;
|
||||
/** The distance between adjacent buttons. */
|
||||
private static final int BUTTON_SPACING = 20;
|
||||
/** The distance of the page numbers from the bottom of the GUI. */
|
||||
private static final int PAGE_NUMBER_INSET = 22;
|
||||
|
||||
// IDEA: Constant dimensions could be converted to JSON like the spell HUD ones
|
||||
|
||||
// Global variables
|
||||
|
||||
/**
|
||||
* The <b>double-page</b> currently being viewed. Each double-page spread counts as a single page, with the inside
|
||||
* of the front cover being page 0.
|
||||
*/
|
||||
private int currentPage = 0;
|
||||
/**
|
||||
* The number of <b>single</b> pages currently in the book. This is calculated on GUI load based on visible sections.
|
||||
*/
|
||||
private int pageCount = 1; // Starts at 1 because the first single-page is the inside of the cover
|
||||
/**
|
||||
* The <b>double-page</b> number where the bookmark is currently set, <b>relative to the section stored in
|
||||
* {@link GuiWizardHandbook#bookmarkSection}</b>. Static because it persists when the book is closed.
|
||||
*/
|
||||
private static int bookmarkPage = 1;
|
||||
/**
|
||||
* A reference to the section in which the bookmark is currently set. Static because it persists when the book is
|
||||
* closed. Storing a section means the bookmark doesn't change location when new sections are unlocked.
|
||||
*/
|
||||
private static Section bookmarkSection;
|
||||
|
||||
// Buttons
|
||||
private GuiButton bookmark, next, previous, nextSection, previousSection, menu;
|
||||
|
||||
// Handbook content
|
||||
|
||||
// As a general rule, I prefer to make static final fields lowercase if they're collections that change, because even
|
||||
// though the collection itself is constant, the stuff in it is not, so being lowercase highlights this difference.
|
||||
|
||||
/**
|
||||
* A map which stores all loaded section objects, including subsections. This gets wiped on resource pack reload and
|
||||
* repopulated with mappings as specified by the handbook JSON file for the current language. The keys in the map
|
||||
* correspond to the keys in the sections object in that file, and are sorted in that order.
|
||||
*/
|
||||
static final Map<String, Section> sections = new LinkedHashMap<>();
|
||||
|
||||
/**
|
||||
* A list which stores all loaded section objects, including subsections. This is an unmodifiable list view of the
|
||||
* values in {@link GuiWizardHandbook#sections}, sorted in the same (page number) order. This exists only to allow
|
||||
* sections to be accessed by ordinal index for the various navigation buttons, hence why it is private.
|
||||
*/
|
||||
private static List<Section> sectionList;
|
||||
|
||||
/**
|
||||
* A map which stores all loaded contents objects. This gets wiped on resource pack reload and repopulated with
|
||||
* mappings as specified by the handbook JSON file for the current language. The keys in the map correspond to the
|
||||
* id strings for the contents objects in that file. This map is not sorted.
|
||||
*/
|
||||
static final Map<String, Contents> contentsList = new HashMap<>();
|
||||
|
||||
/**
|
||||
* A map which stores all loaded hex colour values. This gets wiped on resource pack reload and repopulated with
|
||||
* mappings as specified by the handbook JSON file for the current language. The keys in the map correspond to the
|
||||
* keys in the colours object in that file. This map is not sorted.
|
||||
*/
|
||||
static final Map<String, Integer> colours = new HashMap<>();
|
||||
|
||||
/**
|
||||
* A map which stores all loaded image objects. This gets wiped on resource pack reload and repopulated with
|
||||
* mappings as specified by the handbook JSON file for the current language. The keys in the map correspond to the
|
||||
* keys in the images object in that file. This map is not sorted.
|
||||
*/
|
||||
static final Map<String, Image> images = new HashMap<>();
|
||||
|
||||
/**
|
||||
* A map which stores all loaded crafting recipe objects. This gets wiped on resource pack reload and repopulated
|
||||
* with mappings as specified by the handbook JSON file for the current language. The keys in the map correspond to
|
||||
* the keys in the recipes object in that file. This map is not sorted.
|
||||
*/
|
||||
static final Map<String, CraftingRecipe> recipes = new HashMap<>();
|
||||
|
||||
static String rulerText = "--------------------"; // Assigned here as a fallback
|
||||
|
||||
private static final List<Pair<ItemStack, NonNullList<NonNullList<ItemStack>>>> RECIPES = new ArrayList<>();
|
||||
|
||||
// Reflection
|
||||
|
||||
static final Field ADVANCEMENT_TO_PROGRESS;
|
||||
|
||||
static {
|
||||
// TODO: Obf field name
|
||||
ADVANCEMENT_TO_PROGRESS = ReflectionHelper.findField(ClientAdvancementManager.class, "advancementToProgress");
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds a format tag to the handbook. All occurrences of the given tag string preceded by a # will be replaced with
|
||||
* the result of the given value string on GUI load. The value string, therefore, can be anything that should be
|
||||
* input dynamically, as long as it does not change while the GUI is open. Examples include wizardry's version,
|
||||
* the various element colours and the keys assigned to wizardry's controls.
|
||||
* @param tag The tag string, as defined in the handbook JSON file, excluding the # character. Cannot include spaces.
|
||||
* @param value The string to replace occurrences of the given format tag with. Can include spaces but not the # character.
|
||||
*/
|
||||
public static void addFormatTag(String tag, String value){
|
||||
FORMAT_TAGS.put(tag, value);
|
||||
}
|
||||
|
||||
private static void initFormatTags(){
|
||||
|
||||
addFormatTag("next_spell_key", ClientProxy.NEXT_SPELL.getDisplayName());
|
||||
addFormatTag("previous_spell_key", ClientProxy.PREVIOUS_SPELL.getDisplayName());
|
||||
addFormatTag("mana_per_crystal_minus_30", "" + (Constants.MANA_PER_CRYSTAL - 30));
|
||||
addFormatTag("mana_per_crystal", "" + Constants.MANA_PER_CRYSTAL);
|
||||
addFormatTag("novice_max_charge", "" + Tier.BASIC.maxCharge);
|
||||
addFormatTag("apprentice_max_charge", "" + Tier.APPRENTICE.maxCharge);
|
||||
addFormatTag("advanced_max_charge", "" + Tier.ADVANCED.maxCharge);
|
||||
addFormatTag("master_max_charge", "" + Tier.MASTER.maxCharge);
|
||||
addFormatTag("version", Wizardry.VERSION);
|
||||
addFormatTag("mcversion", Minecraft.getMinecraft().getVersion());
|
||||
|
||||
addFormatTag("colour_novice", "\u00A77");
|
||||
addFormatTag("colour_apprentice", Tier.APPRENTICE.getFormattingCode());
|
||||
addFormatTag("colour_advanced", Tier.ADVANCED.getFormattingCode());
|
||||
addFormatTag("colour_master", Tier.MASTER.getFormattingCode());
|
||||
|
||||
addFormatTag("colour_fire", Element.FIRE.getFormattingCode());
|
||||
addFormatTag("colour_ice", Element.ICE.getFormattingCode());
|
||||
addFormatTag("colour_lightning", Element.LIGHTNING.getFormattingCode());
|
||||
addFormatTag("colour_necromancy", Element.NECROMANCY.getFormattingCode());
|
||||
addFormatTag("colour_earth", Element.EARTH.getFormattingCode());
|
||||
addFormatTag("colour_sorcery", Element.SORCERY.getFormattingCode());
|
||||
addFormatTag("colour_healing", Element.HEALING.getFormattingCode());
|
||||
|
||||
addFormatTag("colour_reset", "\u00A70");
|
||||
}
|
||||
|
||||
// Helper methods
|
||||
|
||||
/**
|
||||
* Converts the given single page index to a double-page index. Inverse of
|
||||
* {@link GuiWizardHandbook#doubleToSinglePage(int, boolean)}.
|
||||
*
|
||||
* @param singlePageIndex The single-page index, which is the same as the page numbers actually displayed.
|
||||
* @return The corresponding double-page index.
|
||||
*/
|
||||
static int singleToDoublePage(int singlePageIndex){
|
||||
// Yes, this is trivial, but if I ever change the numbering it'll be useful. It's also more descriptive.
|
||||
return singlePageIndex / 2;
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts the given double-page index to a single-page index. Inverse of
|
||||
* {@link GuiWizardHandbook#singleToDoublePage(int)}.
|
||||
*
|
||||
* @param doublePageIndex The double-page index, as stored in {@link GuiWizardHandbook#currentPage}.
|
||||
* @param rightHandPage True to return the page on the right (1 greater), false for the left-hand page.
|
||||
* @return The corresponding single-page index.
|
||||
*/
|
||||
static int doubleToSinglePage(int doublePageIndex, boolean rightHandPage){
|
||||
return rightHandPage ? doublePageIndex * 2 + 1 : doublePageIndex * 2;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns whether the given page index refers to a right-hand page or a left-hand page.
|
||||
*
|
||||
* @param page The single-page index, which is the same as the page number actually displayed.
|
||||
* @return True if the given page index refers to a right-hand page, false if it is a left-hand page.
|
||||
*/
|
||||
static boolean isRightPage(int page){
|
||||
return page % 2 == 1;
|
||||
}
|
||||
|
||||
// Drawing
|
||||
|
||||
@Override
|
||||
public void drawScreen(int mouseX, int mouseY, float partialTicks){
|
||||
|
||||
int left = this.width / 2 - GUI_WIDTH / 2;
|
||||
int top = this.height / 2 - GUI_HEIGHT / 2;
|
||||
|
||||
Minecraft.getMinecraft().renderEngine.bindTexture(texture);
|
||||
|
||||
// Main background
|
||||
DrawingUtils.drawTexturedRect(left, top, 0, 0, GUI_WIDTH, GUI_HEIGHT, 512, 256);
|
||||
|
||||
// First page background
|
||||
if(currentPage == 0){
|
||||
DrawingUtils.drawTexturedRect(left, top, 368, 0, GUI_WIDTH / 2, GUI_HEIGHT, 512, 256);
|
||||
previous.visible = false;
|
||||
previousSection.visible = false; // Not worth testing if we're in the first section every frame
|
||||
menu.visible = false;
|
||||
}else{
|
||||
previous.visible = true;
|
||||
previousSection.visible = true;
|
||||
menu.visible = true;
|
||||
}
|
||||
|
||||
// Last page background
|
||||
if(currentPage == singleToDoublePage(pageCount)){
|
||||
DrawingUtils.drawTexturedFlippedRect(left + GUI_WIDTH / 2, top, 368, 0, GUI_WIDTH / 2, GUI_HEIGHT, 512, 256, true, false);
|
||||
next.visible = false;
|
||||
nextSection.visible = false;
|
||||
}else{
|
||||
next.visible = true;
|
||||
nextSection.visible = true;
|
||||
}
|
||||
|
||||
// Page numbers
|
||||
if(currentPage > 0){
|
||||
String pageNumber = "" + doubleToSinglePage(currentPage, false);
|
||||
this.fontRenderer.drawString(pageNumber, left + TEXT_INSET_X + PAGE_WIDTH / 2
|
||||
- fontRenderer.getStringWidth(pageNumber)/2, top + GUI_HEIGHT - PAGE_NUMBER_INSET, DrawingUtils.BLACK);
|
||||
}
|
||||
if(currentPage < singleToDoublePage(pageCount)){
|
||||
String pageNumber = "" + doubleToSinglePage(currentPage, true);
|
||||
this.fontRenderer.drawString(pageNumber, left + GUI_WIDTH - TEXT_INSET_X - PAGE_WIDTH / 2
|
||||
- fontRenderer.getStringWidth(pageNumber)/2, top + GUI_HEIGHT - PAGE_NUMBER_INSET, DrawingUtils.BLACK);
|
||||
}
|
||||
|
||||
// Main content
|
||||
images.values().forEach(i -> i.draw(fontRenderer, currentPage, left, top));
|
||||
contentsList.values().forEach(c -> c.draw(fontRenderer, currentPage, left, top));
|
||||
sections.values().forEach(s -> s.draw(fontRenderer, currentPage, left, top));
|
||||
recipes.values().forEach(r -> r.draw(fontRenderer, itemRender, currentPage, left, top));
|
||||
|
||||
// Buttons
|
||||
super.drawScreen(mouseX, mouseY, partialTicks);
|
||||
|
||||
// Bookmark
|
||||
GlStateManager.color(1, 1, 1, 1);
|
||||
Minecraft.getMinecraft().renderEngine.bindTexture(texture);
|
||||
|
||||
if(currentPage == singleToDoublePage(bookmarkSection.startPage) + bookmarkPage){
|
||||
bookmark.visible = false;
|
||||
DrawingUtils.drawTexturedRect(left + 138, top, 299, 0, 11, 191, 512, 256);
|
||||
}else{
|
||||
bookmark.visible = true;
|
||||
bookmark.x = left + (currentPage > singleToDoublePage(bookmarkSection.startPage) + bookmarkPage ? 130 : 147);
|
||||
DrawingUtils.drawTexturedRect(bookmark.x, top,
|
||||
bookmark.isMouseOver() ? 310 : 288, 0, 11, 191, 512, 256);
|
||||
}
|
||||
|
||||
// Recipe tooltips
|
||||
recipes.values().forEach(r -> r.drawTooltips(this, fontRenderer, itemRender, currentPage, left, top, mouseX, mouseY));
|
||||
|
||||
}
|
||||
|
||||
// GUI Initialisation / Close
|
||||
|
||||
@Override
|
||||
public void onResize(Minecraft minecraft, int width, int height){
|
||||
initGui();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onGuiClosed(){
|
||||
super.onGuiClosed();
|
||||
Keyboard.enableRepeatEvents(false);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void initGui(){
|
||||
|
||||
super.initGui();
|
||||
Keyboard.enableRepeatEvents(true);
|
||||
|
||||
initFormatTags();
|
||||
|
||||
final int left = this.width / 2 - GUI_WIDTH / 2;
|
||||
final int top = this.height / 2 - GUI_HEIGHT / 2;
|
||||
|
||||
recipes.values().forEach(CraftingRecipe::load);
|
||||
|
||||
int nextButtonId = 0;
|
||||
|
||||
this.buttonList.clear();
|
||||
|
||||
this.buttonList.add(next = new GuiButtonTurnPage(nextButtonId++, left + GUI_WIDTH - BUTTON_INSET_X - GuiButtonTurnPage.WIDTH,
|
||||
top + GUI_HEIGHT - BUTTON_INSET_Y - GuiButtonTurnPage.HEIGHT, Type.NEXT_PAGE));
|
||||
|
||||
this.buttonList.add(previous = new GuiButtonTurnPage(nextButtonId++, left + BUTTON_INSET_X,
|
||||
top + GUI_HEIGHT - BUTTON_INSET_Y - GuiButtonTurnPage.HEIGHT, Type.PREVIOUS_PAGE));
|
||||
|
||||
this.buttonList.add(nextSection = new GuiButtonTurnPage(nextButtonId++, left + GUI_WIDTH - BUTTON_INSET_X - GuiButtonTurnPage.WIDTH - BUTTON_SPACING,
|
||||
top + GUI_HEIGHT - BUTTON_INSET_Y - GuiButtonTurnPage.HEIGHT, Type.NEXT_SECTION));
|
||||
|
||||
this.buttonList.add(previousSection = new GuiButtonTurnPage(nextButtonId++, left + BUTTON_INSET_X + BUTTON_SPACING,
|
||||
top + GUI_HEIGHT - BUTTON_INSET_Y - GuiButtonTurnPage.HEIGHT, Type.PREVIOUS_SECTION));
|
||||
|
||||
this.buttonList.add(menu = new GuiButtonTurnPage(nextButtonId++, left + GUI_WIDTH/2 - 28,
|
||||
top + GUI_HEIGHT - BUTTON_INSET_Y - GuiButtonTurnPage.HEIGHT, Type.CONTENTS));
|
||||
|
||||
this.buttonList.add(bookmark = new GuiButtonInvisible(nextButtonId++, left + 130, top + 172, 11, 19) {
|
||||
@Override
|
||||
public void playPressSound(SoundHandler soundHandler){
|
||||
soundHandler.playSound(PositionedSoundRecord.getMasterRecord(WizardrySounds.MISC_PAGE_TURN, 1));
|
||||
}
|
||||
});
|
||||
|
||||
pageCount = 1;
|
||||
|
||||
// Clears instances of all images and recipes
|
||||
images.values().forEach(Image::clearInstances);
|
||||
recipes.values().forEach(CraftingRecipe::clearInstances);
|
||||
|
||||
// Formats all the unlocked sections in order
|
||||
for(Section section : sections.values()){
|
||||
if(section.isUnlocked()){
|
||||
pageCount = section.format(this.fontRenderer, pageCount, left, top);
|
||||
buttonList.addAll(section.getButtons());
|
||||
}
|
||||
}
|
||||
|
||||
contentsList.values().forEach(c -> buttonList.addAll(c.getButtons()));
|
||||
|
||||
this.mc.getSoundHandler().playSound(PositionedSoundRecord.getMasterRecord(WizardrySounds.MISC_BOOK_OPEN, 1));
|
||||
}
|
||||
|
||||
// JSON Parsing / Data Construction
|
||||
|
||||
/**
|
||||
* Called from preInit in the main mod class (via the proxies) to initialise the handbook (parses the JSON file
|
||||
* and constructs the relevant data structures), and again on each resource reload (changing the language triggers
|
||||
* a resource reload).
|
||||
*/
|
||||
public static void loadHandbookFile(IResourceManager manager){
|
||||
|
||||
IResource handbookFile = getHandbookResource(manager);
|
||||
|
||||
if(handbookFile != null){
|
||||
|
||||
// Wipes all the maps before repopulating them
|
||||
images.clear();
|
||||
sections.clear();
|
||||
contentsList.clear();
|
||||
colours.clear();
|
||||
|
||||
bookmarkSection = null; // Also need to wipe the reference to the old bookmarked section
|
||||
|
||||
BufferedReader reader = new BufferedReader(new InputStreamReader(handbookFile.getInputStream()));
|
||||
|
||||
JsonElement je = gson.fromJson(reader, JsonElement.class);
|
||||
JsonObject json = je.getAsJsonObject();
|
||||
|
||||
rulerText = JsonUtils.getString(json, "ruler_text");
|
||||
|
||||
JsonUtils.getJsonObject(json, "colours").entrySet().forEach(e -> colours.put(e.getKey(),
|
||||
Color.decode(e.getValue().getAsString()).getRGB()));
|
||||
|
||||
// Repopulates the remaining maps
|
||||
Image.populate(images, json);
|
||||
CraftingRecipe.populate(recipes, json);
|
||||
Section.populate(sections, json);
|
||||
|
||||
sectionList = Collections.unmodifiableList(new ArrayList<>(sections.values()));
|
||||
|
||||
if(sections.isEmpty()){
|
||||
Wizardry.logger.warn("Handbook has no sections! Aborting loading...");
|
||||
return;
|
||||
}
|
||||
|
||||
// Attempts to find a suitable place to put the bookmark to start with
|
||||
bookmarkSection = sections.get("introduction");
|
||||
if(bookmarkSection == null) bookmarkSection = sectionList.get(0);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves the handbook JSON file for the current language and returns its IResource object. If a handbook file
|
||||
* cannot be found for the current language, a message is printed to the console and the method attempts to retrieve
|
||||
* the default file instead (English-US). If this file cannot be found, the resulting error is printed to the
|
||||
* console and the method returns null.
|
||||
*
|
||||
* @param manager The resource manager instance to use.
|
||||
* @return The handbook JSON file, as an IResource, or null if it was not found.
|
||||
*/
|
||||
private static IResource getHandbookResource(IResourceManager manager){
|
||||
|
||||
IResource handbookFile = null;
|
||||
|
||||
try{
|
||||
handbookFile = manager.getResource(new ResourceLocation(Wizardry.MODID, "texts/handbook_"
|
||||
+ Minecraft.getMinecraft().getLanguageManager().getCurrentLanguage().getLanguageCode() + ".json"));
|
||||
}catch(IOException e){
|
||||
|
||||
Wizardry.logger.info("Wizard handbook JSON file missing for the current language (" + Minecraft.getMinecraft()
|
||||
.getLanguageManager().getCurrentLanguage() + "). Using default (English-US) instead.");
|
||||
|
||||
try{
|
||||
handbookFile = manager.getResource(DEFAULT);
|
||||
}catch(IOException x){
|
||||
Wizardry.logger.error("Couldn't find file: " + DEFAULT + ". The file may be missing; please try re-downloading and reinstalling Wizardry.", x);
|
||||
}
|
||||
}
|
||||
|
||||
return handbookFile;
|
||||
}
|
||||
|
||||
// Controls
|
||||
|
||||
@Override
|
||||
protected void actionPerformed(GuiButton button){
|
||||
|
||||
if(button.enabled){
|
||||
|
||||
if(button == next){
|
||||
if(currentPage < singleToDoublePage(pageCount)) currentPage++;
|
||||
|
||||
}else if(button == previous){
|
||||
if(currentPage > 0) currentPage--;
|
||||
|
||||
}else if(button == nextSection || button == previousSection){
|
||||
|
||||
Section currentSection = null;
|
||||
|
||||
for(Section section : sections.values()){
|
||||
// We always want this button to do something, and taking the right-hand page means it always does
|
||||
if(section.containsPage(doubleToSinglePage(currentPage, true))){
|
||||
currentSection = section;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if(currentSection != null){
|
||||
|
||||
int index = sectionList.indexOf(currentSection);
|
||||
|
||||
if(button == nextSection && index + 1 < sections.size()){
|
||||
currentPage = singleToDoublePage(sectionList.get(index + 1).startPage);
|
||||
}else if(index > 0){
|
||||
currentPage = singleToDoublePage(sectionList.get(index - 1).startPage);
|
||||
}
|
||||
}
|
||||
|
||||
}else if(button == menu){
|
||||
currentPage = singleToDoublePage(sections.get("main_contents").startPage);
|
||||
|
||||
}else if(button == bookmark && bookmarkSection != null){
|
||||
currentPage = singleToDoublePage(bookmarkSection.startPage) + bookmarkPage;
|
||||
|
||||
}else{
|
||||
if(button instanceof GuiButtonHyperlink.Internal){
|
||||
currentPage = singleToDoublePage(((GuiButtonHyperlink.Internal)button).target.startPage);
|
||||
}else if(button instanceof GuiButtonHyperlink.External){
|
||||
this.handleComponentClick(((GuiButtonHyperlink.External)button).link);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void mouseClicked(int mouseX, int mouseY, int mouseButton) throws IOException{
|
||||
if(mouseButton == 1){
|
||||
// Right-clicking of bookmark
|
||||
if(bookmark.mousePressed(this.mc, mouseX, mouseY)){
|
||||
|
||||
this.selectedButton = bookmark;
|
||||
|
||||
for(Section section : sections.values()){
|
||||
// The bookmark is assumed to bookmark the left-hand page
|
||||
if(section.containsPage(doubleToSinglePage(bookmarkPage, false))) bookmarkSection = section;
|
||||
}
|
||||
|
||||
bookmarkPage = currentPage - singleToDoublePage(bookmarkSection.startPage);
|
||||
}
|
||||
}else{
|
||||
super.mouseClicked(mouseX, mouseY, mouseButton);
|
||||
}
|
||||
}
|
||||
|
||||
// Overridden to make it public
|
||||
@Override
|
||||
public void renderToolTip(ItemStack stack, int x, int y){
|
||||
super.renderToolTip(stack, x, y);
|
||||
}
|
||||
|
||||
@SubscribeEvent
|
||||
public static void onAdvancementEvent(AdvancementEvent event){
|
||||
sections.values().forEach(s -> s.onAdvancement(event.getEntityPlayer(), event.getAdvancement()));
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,119 @@
|
||||
package electroblob.wizardry.client.gui.handbook;
|
||||
|
||||
import com.google.gson.JsonElement;
|
||||
import com.google.gson.JsonObject;
|
||||
import com.google.gson.JsonSyntaxException;
|
||||
import electroblob.wizardry.client.DrawingUtils;
|
||||
import net.minecraft.client.Minecraft;
|
||||
import net.minecraft.client.gui.FontRenderer;
|
||||
import net.minecraft.client.renderer.GlStateManager;
|
||||
import net.minecraft.util.JsonUtils;
|
||||
import net.minecraft.util.ResourceLocation;
|
||||
|
||||
import java.util.HashSet;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
|
||||
class Image {
|
||||
|
||||
// Final fields are mandatory, the rest are optional
|
||||
private final ResourceLocation location;
|
||||
private final int width, height;
|
||||
private int textureWidth, textureHeight;
|
||||
private int u = 0, v = 0;
|
||||
private String caption = "";
|
||||
// Derived fields, not specifically defined in JSON
|
||||
private final Set<int[]> instances = new HashSet<>();
|
||||
|
||||
private static final int CAPTION_OFFSET = 4;
|
||||
|
||||
private Image(ResourceLocation location, int width, int height){
|
||||
this.location = location;
|
||||
this.width = width;
|
||||
this.height = height;
|
||||
}
|
||||
|
||||
/** Returns the width of the image. */
|
||||
int getWidth(){
|
||||
return width;
|
||||
}
|
||||
|
||||
/** Returns the total height of the image, including caption if it has one. */
|
||||
int getHeight(FontRenderer font){
|
||||
return caption.isEmpty() ? height : height + CAPTION_OFFSET + font.FONT_HEIGHT;
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds an instance of this image to the list.
|
||||
*
|
||||
* @param page The index of the <b>single</b> page this image is on.
|
||||
* @param x The x-coordinate of the top-left corner of the image, <i>relative</i> to the top-left corner of the GUI.
|
||||
* @param y The y-coordinate of the top-left corner of the image, <i>relative</i> to the top-left corner of the GUI.
|
||||
*/
|
||||
void addInstance(int page, int x, int y){
|
||||
instances.add(new int[]{page, x, y});
|
||||
}
|
||||
|
||||
/** Removes all instances of this image from the list. */
|
||||
void clearInstances(){
|
||||
instances.clear();
|
||||
}
|
||||
|
||||
/**
|
||||
* Draws all instances of this image that are located on the given double-page spread.
|
||||
*
|
||||
* @param font The font renderer object.
|
||||
* @param doublePage The double-page index of the page to be drawn.
|
||||
* @param left The x coordinate of the left side of the GUI.
|
||||
* @param top The y coordinate of the top of the GUI.
|
||||
*/
|
||||
void draw(FontRenderer font, int doublePage, int left, int top){
|
||||
for(int[] instance : instances){
|
||||
if(GuiWizardHandbook.singleToDoublePage(instance[0]) == doublePage){
|
||||
Minecraft.getMinecraft().renderEngine.bindTexture(location);
|
||||
GlStateManager.color(1, 1, 1, 1);
|
||||
DrawingUtils.drawTexturedRect(left + instance[1], top + instance[2], u, v, width, height, textureWidth, textureHeight);
|
||||
font.drawString("\u00A7o" + caption, left + instance[1] + width/ 2 - font.getStringWidth(caption)/2,
|
||||
top + instance[2] + height + CAPTION_OFFSET, GuiWizardHandbook.colours.get("caption"));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Parses the given JSON object and constructs a new {@code Image} from it, setting all the relevant fields
|
||||
* and references.
|
||||
*
|
||||
* @param json A JSON object representing the image to be constructed. This must contain at least a "location"
|
||||
* string.
|
||||
* @return The resulting {@code Image} object.
|
||||
* @throws JsonSyntaxException if at any point the JSON object is found to be invalid.
|
||||
*/
|
||||
static Image fromJson(JsonObject json){
|
||||
|
||||
Image image = new Image(new ResourceLocation(JsonUtils.getString(json, "location")),
|
||||
JsonUtils.getInt(json, "width"), JsonUtils.getInt(json, "height"));
|
||||
|
||||
image.u = JsonUtils.getInt(json, "u", 0);
|
||||
image.v = JsonUtils.getInt(json, "v", 0);
|
||||
image.textureWidth = JsonUtils.getInt(json, "texture_width", image.width);
|
||||
image.textureHeight = JsonUtils.getInt(json, "texture_height", image.height);
|
||||
image.caption = JsonUtils.getString(json, "caption", "");
|
||||
|
||||
|
||||
return image;
|
||||
}
|
||||
|
||||
static void populate(Map<String, Image> map, JsonObject json){
|
||||
|
||||
JsonObject sectionsObject = JsonUtils.getJsonObject(json, "images");
|
||||
|
||||
// Need to iterate over these since we don't know what they're called or how many there are
|
||||
for(Map.Entry<String, JsonElement> entry : sectionsObject.entrySet()){
|
||||
|
||||
String key = entry.getKey(); // Find out what each element is called, this will be the sections map key
|
||||
|
||||
Image image = fromJson(entry.getValue().getAsJsonObject());
|
||||
map.put(key, image);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,429 @@
|
||||
package electroblob.wizardry.client.gui.handbook;
|
||||
|
||||
import com.google.common.collect.Streams;
|
||||
import com.google.gson.JsonElement;
|
||||
import com.google.gson.JsonObject;
|
||||
import com.google.gson.JsonSyntaxException;
|
||||
import electroblob.wizardry.Wizardry;
|
||||
import electroblob.wizardry.client.DrawingUtils;
|
||||
import electroblob.wizardry.util.WizardryUtilities;
|
||||
import net.minecraft.advancements.Advancement;
|
||||
import net.minecraft.advancements.AdvancementProgress;
|
||||
import net.minecraft.client.Minecraft;
|
||||
import net.minecraft.client.gui.FontRenderer;
|
||||
import net.minecraft.client.gui.GuiButton;
|
||||
import net.minecraft.entity.player.EntityPlayer;
|
||||
import net.minecraft.util.JsonUtils;
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
|
||||
import java.util.*;
|
||||
|
||||
/**
|
||||
* Instances of this class represent sections in the wizard's handbook. As of wizardry 4.2, this class handles
|
||||
* everything within the section itself, including JSON parsing, unlock triggers and drawing the actual rawText.
|
||||
* Sections may now also be nested and have other elements within them, such as images and a table of contents, a
|
||||
* behaviour which is also handled within this class.
|
||||
* <p>
|
||||
* The formatting of the book is now done 'dynamically' - that is, the exact positions and page numbers of
|
||||
* sections, images and so on are determined on GUI load and depend on which of the previous sections have been
|
||||
* unlocked, amongst other factors. This means that all of the unlocked sections must be formatted in order on GUI
|
||||
* load, so that each section knows the previous section's length and therefore where to start.
|
||||
*
|
||||
* @author Electroblob
|
||||
* @since Wizardry 4.2
|
||||
*/
|
||||
// Because these are now generated on resource pack reload (not on handbook open, as before), this class must now
|
||||
// be static
|
||||
class Section {
|
||||
|
||||
// Final fields are mandatory (none here though), the rest are optional
|
||||
String title;
|
||||
private String[] rawText;
|
||||
private Contents contents;
|
||||
private Advancement[] triggers;
|
||||
private Map<String, Section> subsections;
|
||||
private boolean centreX, centreY;
|
||||
|
||||
// Derived fields, not explicitly defined in JSON
|
||||
/** The <b>single-page</b> index of the first page of this section. */
|
||||
int startPage;
|
||||
private final List<List<GuiButton>> buttons;
|
||||
/**
|
||||
* A list of <b>single</b> pages, which are themselves lists of paragraphs (each paragraph is a single
|
||||
* string which may include line breaks and other escape characters).
|
||||
*/
|
||||
private final List<List<String>> pages;
|
||||
|
||||
private Section(){
|
||||
this.buttons = new ArrayList<>();
|
||||
this.pages = new ArrayList<>();
|
||||
this.subsections = new LinkedHashMap<>();
|
||||
}
|
||||
|
||||
Collection<GuiButton> getButtons(){
|
||||
return WizardryUtilities.flatten(buttons);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns true if the given page is within this section, false if not.
|
||||
*/
|
||||
boolean containsPage(int page){
|
||||
return startPage <= page && startPage + pages.size() > page;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns true if this section is unlocked for the client player, false if not. Always returns true if
|
||||
* handbook progression is disabled in the config.
|
||||
*/
|
||||
boolean isUnlocked(){
|
||||
|
||||
if(!Wizardry.settings.handbookProgression) return true; // Always unlocked if handbook progression is off
|
||||
|
||||
// A section is automatically unlocked if one of its subsections is unlocked
|
||||
for(Section subsection : subsections.values()){
|
||||
if(subsection.isUnlocked()) return true;
|
||||
}
|
||||
|
||||
if(triggers == null) return true; // If no triggers were defined, the section is unlocked from the start
|
||||
|
||||
for(Advancement trigger : triggers){
|
||||
|
||||
try{
|
||||
|
||||
// TESTME: Is this properly synced or do we need to do that as well?
|
||||
Map<Advancement, AdvancementProgress> advancements = (Map)GuiWizardHandbook.ADVANCEMENT_TO_PROGRESS.get(Minecraft.getMinecraft().player.connection.getAdvancementManager());
|
||||
|
||||
if(advancements.get(trigger).isDone()){
|
||||
return true;
|
||||
}
|
||||
|
||||
}catch(IllegalAccessException e){
|
||||
e.printStackTrace();
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Actually draws the contents of the given section for the given double-page spread. Will do nothing if the
|
||||
* given page is outside of this section.
|
||||
*
|
||||
* @param font The font renderer object.
|
||||
* @param doublePage The index of the <b>double-page</b> to be drawn.
|
||||
* @param left The x coordinate of the left side of the GUI.
|
||||
* @param top The y coordinate of the top of the GUI.
|
||||
*/
|
||||
// This method is supposed to be 'idiot-proof' in the sense that the code calling it need not check whether the
|
||||
// section actually needs drawing, so it can just dumbly call draw(...) for all the sections in order.
|
||||
void draw(FontRenderer font, int doublePage, int left, int top){
|
||||
|
||||
// Show/hide buttons
|
||||
|
||||
int i = 0;
|
||||
|
||||
for(List<GuiButton> list : buttons){
|
||||
final int i1 = i++;
|
||||
list.forEach(b -> b.visible = GuiWizardHandbook.singleToDoublePage(startPage + i1) == doublePage);
|
||||
}
|
||||
|
||||
int leftIndex = GuiWizardHandbook.doubleToSinglePage(doublePage, false);
|
||||
// Relative indices of the pages to be rendered - often these will be outside the section entirely
|
||||
int[] visiblePages = {leftIndex - startPage, leftIndex - startPage + 1};
|
||||
|
||||
for(int page : visiblePages){
|
||||
|
||||
if(page >= 0 && page < pages.size()){
|
||||
|
||||
List<String> lines = pages.get(page);
|
||||
|
||||
int x = left + (GuiWizardHandbook.isRightPage(startPage + page) ? GuiWizardHandbook.GUI_WIDTH - GuiWizardHandbook.TEXT_INSET_X - GuiWizardHandbook.PAGE_WIDTH : GuiWizardHandbook.TEXT_INSET_X);
|
||||
int y = top + GuiWizardHandbook.TEXT_INSET_Y;
|
||||
if(centreY) y += GuiWizardHandbook.PAGE_HEIGHT / 2 - lines.size() / 2 * font.FONT_HEIGHT;
|
||||
|
||||
for(String line : lines){
|
||||
|
||||
int lx = centreX ? x + GuiWizardHandbook.PAGE_WIDTH / 2 - font.getStringWidth(line) / 2 : x;
|
||||
|
||||
font.drawString(line, lx, y, DrawingUtils.BLACK, false);
|
||||
|
||||
y += font.FONT_HEIGHT;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Called on GUI load to format the section, contents tables and other elements, <b>excluding</b> subsections.
|
||||
* Does not perform any actual drawing.
|
||||
*
|
||||
* @param font The font renderer object, for measurement purposes.
|
||||
* @param startPage The index of the first page (single side, not double-page) of this section.
|
||||
* @param left The x coordinate of the left side of the GUI.
|
||||
* @param top The y coordinate of the top of the GUI.
|
||||
* @return The <b>single-page</b> index of the next blank page after the end of this section.
|
||||
* @throws JsonSyntaxException if at any point the formatting is found to be invalid.
|
||||
*/
|
||||
int format(FontRenderer font, int startPage, int left, int top){
|
||||
|
||||
this.buttons.clear();
|
||||
this.pages.clear();
|
||||
|
||||
// FONT_HEIGHT may change between fonts, so this is calculated here. With the default font it's 14.
|
||||
final int maxLineNumber = GuiWizardHandbook.PAGE_HEIGHT / font.FONT_HEIGHT;
|
||||
|
||||
this.startPage = startPage;
|
||||
|
||||
// First everything is added to a single list of lines, then it is split into pages.
|
||||
List<String> lines = new ArrayList<>();
|
||||
|
||||
// Adds the header if present
|
||||
if(!this.title.isEmpty()){
|
||||
lines.add(this.title);
|
||||
lines.add(GuiWizardHandbook.rulerText);
|
||||
}
|
||||
|
||||
// Adds space for the contents if it exists
|
||||
if(this.contents != null){
|
||||
lines.addAll(Collections.nCopies(this.contents.format(font, startPage, lines.size(), left, top), ""));
|
||||
// Line break between contents and first paragraph
|
||||
if((lines.size() % maxLineNumber) != 0) lines.add("");
|
||||
}
|
||||
|
||||
if(this.rawText != null){
|
||||
// Paragraphs are defined as a JSON list because it makes it easier to arrange them properly across pages
|
||||
// - using multiple line breaks would mean having to find and remove them when at the top of a page.
|
||||
for(String paragraph : this.rawText){
|
||||
|
||||
// (lines.size() % maxLineNumber) gives the number of lines on the current page
|
||||
// (lines.size() / maxLineNumber) gives the index of the current page minus the value of startPage
|
||||
|
||||
// Formats the paragraph
|
||||
|
||||
String raw = paragraph; // For error messages
|
||||
|
||||
// Images (images must be separate paragraphs)
|
||||
|
||||
if(paragraph.startsWith(GuiWizardHandbook.FORMAT_MARKER + GuiWizardHandbook.IMAGE_TAG)){
|
||||
|
||||
String[] arguments = paragraph.split("\\s", 2);
|
||||
|
||||
if(arguments.length < 2) throw new JsonSyntaxException("Missing image name in string "
|
||||
+ StringUtils.abbreviate(raw, 50));
|
||||
|
||||
Image image = GuiWizardHandbook.images.get(arguments[1]);
|
||||
if(image == null) throw new JsonSyntaxException("Image with id " + arguments[1] + "is undefined");
|
||||
|
||||
// Starts a new page if the image will not fit on the current one
|
||||
if((lines.size() % maxLineNumber) * font.FONT_HEIGHT + image.getHeight(font) > GuiWizardHandbook.PAGE_HEIGHT){
|
||||
// Remaining number of lines on the page
|
||||
lines.addAll(Collections.nCopies(maxLineNumber - (lines.size() % maxLineNumber), ""));
|
||||
}
|
||||
|
||||
if(image.getWidth() > GuiWizardHandbook.PAGE_WIDTH) Wizardry.logger.warn("Image with id " + arguments[1]
|
||||
+ "has a width (" + image.getWidth() + ") greater than the maximum page width (" + GuiWizardHandbook.PAGE_WIDTH
|
||||
+ "), it will extend beyond the page area.");
|
||||
|
||||
if(image.getHeight(font) > GuiWizardHandbook.PAGE_HEIGHT) Wizardry.logger.warn("Image with id " + arguments[1]
|
||||
+ "has a height (" + image.getHeight(font) + ") greater than the maximum page height (" + GuiWizardHandbook.PAGE_HEIGHT
|
||||
+ "), it will extend beyond the page area.");
|
||||
|
||||
int page = startPage + (lines.size() / maxLineNumber);
|
||||
|
||||
image.addInstance(page, GuiWizardHandbook.PAGE_WIDTH / 2 - image.getWidth() / 2
|
||||
+ (GuiWizardHandbook.isRightPage(page) ? GuiWizardHandbook.GUI_WIDTH - GuiWizardHandbook.TEXT_INSET_X - GuiWizardHandbook.PAGE_WIDTH : GuiWizardHandbook.TEXT_INSET_X),
|
||||
GuiWizardHandbook.TEXT_INSET_Y + (lines.size() % maxLineNumber) * font.FONT_HEIGHT);
|
||||
|
||||
// Height of the image in lines, rounded up
|
||||
// Uses a single space instead of an empty string so that the page trimming doesn't remove them
|
||||
lines.addAll(Collections.nCopies(image.getHeight(font) / font.FONT_HEIGHT, " "));
|
||||
lines.add(""); // The last one is removable though, since it's actually extra space
|
||||
|
||||
// Recipes (recipes must be separate paragraphs)
|
||||
}else if(paragraph.startsWith(GuiWizardHandbook.FORMAT_MARKER + GuiWizardHandbook.RECIPE_TAG)){
|
||||
|
||||
String[] arguments = paragraph.split("\\s", 2);
|
||||
|
||||
if(arguments.length < 2) throw new JsonSyntaxException("Missing recipe name in string "
|
||||
+ StringUtils.abbreviate(raw, 50));
|
||||
|
||||
CraftingRecipe recipe = GuiWizardHandbook.recipes.get(arguments[1]);
|
||||
if(recipe == null) throw new JsonSyntaxException("Recipe with id " + arguments[1] + "is undefined");
|
||||
|
||||
// Starts a new page if the recipe will not fit on the current one
|
||||
if((lines.size() % maxLineNumber) * font.FONT_HEIGHT + CraftingRecipe.HEIGHT > GuiWizardHandbook.PAGE_HEIGHT){
|
||||
// Remaining number of lines on the page
|
||||
lines.addAll(Collections.nCopies(maxLineNumber - (lines.size() % maxLineNumber), ""));
|
||||
}
|
||||
|
||||
int page = startPage + (lines.size() / maxLineNumber);
|
||||
|
||||
recipe.addInstance(page, GuiWizardHandbook.PAGE_WIDTH / 2 - CraftingRecipe.WIDTH / 2
|
||||
+ (GuiWizardHandbook.isRightPage(page) ? GuiWizardHandbook.GUI_WIDTH - GuiWizardHandbook.TEXT_INSET_X - GuiWizardHandbook.PAGE_WIDTH : GuiWizardHandbook.TEXT_INSET_X),
|
||||
GuiWizardHandbook.TEXT_INSET_Y + (lines.size() % maxLineNumber) * font.FONT_HEIGHT);
|
||||
|
||||
// Height of the recipe in lines, rounded up
|
||||
// Uses a single space instead of an empty string so that the page trimming doesn't remove them
|
||||
lines.addAll(Collections.nCopies(CraftingRecipe.HEIGHT / font.FONT_HEIGHT, " "));
|
||||
// This time we're not adding an extra space because it's not really needed
|
||||
|
||||
}else{ // All other paragraphs
|
||||
|
||||
// Hyperlinks
|
||||
|
||||
int linkStart;
|
||||
|
||||
while((linkStart = paragraph.indexOf(GuiWizardHandbook.HYPERLINK_MARKER)) > -1){ // Ooh an assignment and a comparison in one...
|
||||
|
||||
int linkEnd = paragraph.indexOf(GuiWizardHandbook.HYPERLINK_MARKER, linkStart + 1);
|
||||
|
||||
if(linkEnd < 0) throw new JsonSyntaxException("Un-closed hyperlink marker in string "
|
||||
+ StringUtils.abbreviate(raw, 50));
|
||||
|
||||
List<String> upToLink = font.listFormattedStringToWidth(paragraph.substring(0, linkStart), GuiWizardHandbook.PAGE_WIDTH);
|
||||
|
||||
String linkRaw = paragraph.substring(linkStart, linkEnd + 1);
|
||||
String[] arguments = paragraph.substring(linkStart + 1, linkEnd).split("\\s", 2);
|
||||
String suffix = paragraph.substring(linkEnd).split("\\s", 2)[0];
|
||||
|
||||
// The index of the single page currently being formatted, relative to the section
|
||||
int pageRelative = (lines.size() + upToLink.size() - 1) / maxLineNumber;
|
||||
// The overall index of the single page currently being formatted
|
||||
int page = startPage + pageRelative;
|
||||
|
||||
int x = GuiWizardHandbook.isRightPage(page) ? left + GuiWizardHandbook.GUI_WIDTH - GuiWizardHandbook.TEXT_INSET_X - GuiWizardHandbook.PAGE_WIDTH : left + GuiWizardHandbook.TEXT_INSET_X;
|
||||
int y = top + GuiWizardHandbook.TEXT_INSET_Y + (((lines.size() + upToLink.size() - 1) % maxLineNumber)) * font.FONT_HEIGHT;
|
||||
|
||||
// Adds any missing sub-lists
|
||||
while(this.buttons.size() <= pageRelative){
|
||||
this.buttons.add(new ArrayList<>());
|
||||
}
|
||||
|
||||
// The button id only does what you use it for, so we're just not using it at all.
|
||||
this.buttons.get(pageRelative).add(GuiButtonHyperlink.create(x, y, font, upToLink, arguments, suffix));
|
||||
|
||||
// The link button should exactly overlay the display rawText in the main string
|
||||
// If the link has no display rawText specified, it displays the unformatted target string
|
||||
paragraph = paragraph.replace(linkRaw, arguments[arguments.length - 1]);
|
||||
}
|
||||
|
||||
// Formatting
|
||||
for(Map.Entry<String, String> entry : GuiWizardHandbook.FORMAT_TAGS.entrySet()){
|
||||
paragraph = paragraph.replace(GuiWizardHandbook.FORMAT_MARKER + entry.getKey(), entry.getValue());
|
||||
}
|
||||
|
||||
lines.addAll(font.listFormattedStringToWidth(paragraph, GuiWizardHandbook.PAGE_WIDTH));
|
||||
}
|
||||
|
||||
// Line break between paragraphs (the last one will just be deleted later)
|
||||
if((lines.size() % maxLineNumber) != 0) lines.add("");
|
||||
}
|
||||
}
|
||||
|
||||
// Splits lines into pages
|
||||
|
||||
List<String> page = new ArrayList<>();
|
||||
pages.add(page);
|
||||
|
||||
while(!lines.isEmpty()){
|
||||
|
||||
if(page.size() == maxLineNumber){
|
||||
// Removes blank lines at the end of the page
|
||||
while(page.get(page.size() - 1).isEmpty()) page.remove(page.size() - 1);
|
||||
// Adds a new page
|
||||
pages.add(page = new ArrayList<>());
|
||||
}
|
||||
|
||||
String line = lines.remove(0);
|
||||
|
||||
// Prevents blank lines at the start of the page
|
||||
if(!page.isEmpty() || !line.isEmpty()) page.add(line);
|
||||
}
|
||||
|
||||
return startPage + pages.size();
|
||||
}
|
||||
|
||||
/**
|
||||
* Parses the given JSON object and constructs a new {@code Section} from it, setting all the relevant fields
|
||||
* and references. This method converts the JSON object to a {@code Section} object and retrieves any resources;
|
||||
* the section is not formatted in any way until GUI load, in {@link Section#format(FontRenderer, int, int, int)}.
|
||||
*
|
||||
* @param json A JSON object representing the section to be constructed. This must contain at least a "title"
|
||||
* string.
|
||||
* @return The resulting {@code Section} object.
|
||||
* @throws JsonSyntaxException if at any point the JSON object is found to be invalid.
|
||||
*/
|
||||
static Section fromJson(JsonObject json){
|
||||
|
||||
Section section = new Section();
|
||||
|
||||
section.title = JsonUtils.getString(json, "title", "");
|
||||
|
||||
if(JsonUtils.hasField(json, "include_in_contents")){
|
||||
|
||||
String id = JsonUtils.getString(json, "include_in_contents");
|
||||
|
||||
Contents belongsTo = GuiWizardHandbook.contentsList.get(id);
|
||||
|
||||
if(belongsTo == null){
|
||||
throw new JsonSyntaxException("Expected include_in_contents to be the id of a previously defined contents, but no contents with the id " + id + " exists yet.");
|
||||
}else{
|
||||
belongsTo.addEntry(section);
|
||||
}
|
||||
}
|
||||
|
||||
if(JsonUtils.hasField(json, "contents")){
|
||||
section.contents = Contents.fromJson(JsonUtils.getJsonObject(json, "contents"));
|
||||
GuiWizardHandbook.contentsList.put(section.contents.id, section.contents);
|
||||
}
|
||||
|
||||
if(JsonUtils.hasField(json, "text")){
|
||||
// TESTME: Does this always preserve order?
|
||||
section.rawText = Streams.stream(JsonUtils.getJsonArray(json, "text"))
|
||||
.map(e -> JsonUtils.getString(e, "element of array rawText"))
|
||||
.toArray(String[]::new);
|
||||
}
|
||||
|
||||
// TODO: Triggers
|
||||
|
||||
if(JsonUtils.hasField(json, "centre")){
|
||||
JsonObject centre = JsonUtils.getJsonObject(json,"centre");
|
||||
section.centreX = JsonUtils.getBoolean(centre, "x", false);
|
||||
section.centreY = JsonUtils.getBoolean(centre, "y", false);
|
||||
}
|
||||
|
||||
// The only benefit of having subsections (other than logical grouping) is that the parent section can
|
||||
// automatically be unlocked if one of the subsections is.
|
||||
if(JsonUtils.hasField(json, "sections")){
|
||||
populate(section.subsections, json);
|
||||
}
|
||||
|
||||
return section;
|
||||
}
|
||||
|
||||
static void populate(Map<String, Section> map, JsonObject json){
|
||||
|
||||
JsonObject sectionsObject = JsonUtils.getJsonObject(json, "sections");
|
||||
|
||||
// Need to iterate over these since we don't know what they're called or how many there are
|
||||
for(Map.Entry<String, JsonElement> entry : sectionsObject.entrySet()){
|
||||
|
||||
String key = entry.getKey(); // Find out what each element is called, this will be the sections map key
|
||||
|
||||
Section section = fromJson(entry.getValue().getAsJsonObject());
|
||||
map.put(key, section);
|
||||
map.putAll(section.subsections);
|
||||
}
|
||||
}
|
||||
|
||||
public void onAdvancement(EntityPlayer player, Advancement advancement){
|
||||
Minecraft minecraft = Minecraft.getMinecraft();
|
||||
//System.out.println(title);
|
||||
if(triggers != null && Arrays.asList(triggers).contains(advancement) && player == minecraft.player){
|
||||
//minecraft.getToastGui().drawToast(IToast);
|
||||
|
||||
// Debug
|
||||
//System.out.println(title);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,528 @@
|
||||
{
|
||||
"ruler_text": "--------------------",
|
||||
|
||||
"colours": {
|
||||
"text": "#000000",
|
||||
"caption": "#666666",
|
||||
"hyperlink": "#601ba0",
|
||||
"highlight": "#dd4c1d"
|
||||
},
|
||||
|
||||
"images": {
|
||||
"workbench": {
|
||||
"location": "ebwizardry:textures/gui/arcane_workbench_picture.png",
|
||||
"caption": "The Arcane Workbench",
|
||||
"u": 0,
|
||||
"v": 0,
|
||||
"width": 110,
|
||||
"height": 110
|
||||
},
|
||||
"crystal_ore": {
|
||||
"location": "ebwizardry:textures/gui/ore_picture.png",
|
||||
"caption": "Crystal Ore",
|
||||
"u": 0,
|
||||
"v": 0,
|
||||
"width": 64,
|
||||
"height": 64
|
||||
},
|
||||
"magic_crystal": {
|
||||
"location": "ebwizardry:textures/items/crystal_magic.png",
|
||||
"caption": "A Magic Crystal",
|
||||
"u": 6,
|
||||
"v": 6,
|
||||
"width": 36,
|
||||
"height": 33,
|
||||
"texture_width": 48,
|
||||
"texture_height": 48
|
||||
},
|
||||
"magic_wand": {
|
||||
"location": "ebwizardry:textures/items/wand_basic.png",
|
||||
"caption": "A Magic Wand",
|
||||
"u": 0,
|
||||
"v": 0,
|
||||
"width": 64,
|
||||
"height": 64
|
||||
},
|
||||
"crystal_flower": {
|
||||
"location": "ebwizardry:textures/gui/flower_picture.png",
|
||||
"caption": "A Crystal Flower",
|
||||
"u": 0,
|
||||
"v": 0,
|
||||
"width": 64,
|
||||
"height": 64
|
||||
}
|
||||
},
|
||||
|
||||
"recipes": {
|
||||
"arcane_workbench": {
|
||||
"location": "ebwizardry:arcane_workbench"
|
||||
},
|
||||
"magic_wand": {
|
||||
"location": "ebwizardry:magic_wand"
|
||||
},
|
||||
"magic_missile_spell_book": {
|
||||
"location": "ebwizardry:magic_missile_spell_book"
|
||||
},
|
||||
"wizard_handbook": {
|
||||
"location": "ebwizardry:wizard_handbook"
|
||||
},
|
||||
"crystal_flower_to_crystals": {
|
||||
"location": "ebwizardry:crystal_flower_to_crystals"
|
||||
},
|
||||
"mana_flask": {
|
||||
"location": "ebwizardry:mana_flask"
|
||||
},
|
||||
"transportation_stone": {
|
||||
"location": "ebwizardry:transportation_stone"
|
||||
},
|
||||
"magic_silk": {
|
||||
"location": "ebwizardry:magic_silk"
|
||||
},
|
||||
"wizard_hat": {
|
||||
"location": "ebwizardry:wizard_hat"
|
||||
},
|
||||
"wizard_robe": {
|
||||
"location": "ebwizardry:wizard_robe"
|
||||
},
|
||||
"wizard_leggings": {
|
||||
"location": "ebwizardry:wizard_leggings"
|
||||
},
|
||||
"wizard_boots": {
|
||||
"location": "ebwizardry:wizard_boots"
|
||||
},
|
||||
"blank_scroll": {
|
||||
"location": "ebwizardry:blank_scroll"
|
||||
},
|
||||
"firebomb": {
|
||||
"location": "ebwizardry:firebomb"
|
||||
},
|
||||
"poison_bomb": {
|
||||
"location": "ebwizardry:poison_bomb"
|
||||
},
|
||||
"smoke_bomb": {
|
||||
"location": "ebwizardry:smoke_bomb"
|
||||
}
|
||||
},
|
||||
|
||||
"sections": {
|
||||
|
||||
"inside_cover": {
|
||||
"centre": {
|
||||
"x": true,
|
||||
"y": true
|
||||
},
|
||||
"text": [
|
||||
|
||||
"The Wizard's Handbook",
|
||||
|
||||
"By Electroblob"
|
||||
]
|
||||
},
|
||||
|
||||
"introduction": {
|
||||
"title": "Introduction",
|
||||
"text": [
|
||||
|
||||
"Greetings, wizard! This book explains the many ways of the arcane and how to use them. This is no ordinary book, though - its pages will materialise before you as you discover more about the magical world.",
|
||||
|
||||
"Use the arrow buttons to turn between pages, and the double-arrow buttons to quickly flip between sections. Use the central menu button to return to the main contents page.",
|
||||
|
||||
"Click on any purple link text to jump straight to the relevant page. Right-click the bookmark to move it to the current page, and left-click to return to the bookmarked page."
|
||||
]
|
||||
},
|
||||
|
||||
"main_contents": {
|
||||
"title": "Contents",
|
||||
"contents": {
|
||||
"id": "main_contents",
|
||||
"hyperlinks": true,
|
||||
"page_numbers": true,
|
||||
"separator": "."
|
||||
}
|
||||
},
|
||||
|
||||
"setting_up": {
|
||||
"title": "Setting Up",
|
||||
"include_in_contents": "main_contents",
|
||||
"text": [
|
||||
|
||||
"To get started with wizardry, you will need a few things:",
|
||||
|
||||
"- A magic wand, crafted with a gold nugget, a stick, and a magic crystal.",
|
||||
|
||||
"- An @arcane_workbench arcane workbench@, crafted with 3 stone, 1 lapis lazuli block, 2 magic crystals, 2 gold nuggets and 1 purple carpet.",
|
||||
|
||||
"- A @spells spell@ book. These can be found in chests, dropped as loot, purchased from wizards, or you can make a beginner's spell, magic missile, with a book and 4 magic crystals.",
|
||||
|
||||
"You will also need a bunch more magic crystals to supply your wand with @mana@."
|
||||
]
|
||||
},
|
||||
|
||||
"mana": {
|
||||
"title": "Mana",
|
||||
"include_in_contents": "main_contents",
|
||||
"triggers": [
|
||||
"ebwizardry:crystal"
|
||||
],
|
||||
"text": [
|
||||
|
||||
"Mana is the arcane energy that gives wizards their powers. It is not a physical substance in its own right; rather, it is an everpresent aura which permeates everything in the world. In particular, it manifests itself in naturally occurring crystals, which can be found embedded in rocks underground. @wands Wands@ can store mana within them, and this mana is channelled into the @spells@ that you cast. Different spells require different amounts of mana, depending on how powerful they are and how long they last.",
|
||||
|
||||
"#image crystal_ore",
|
||||
|
||||
"#image magic_crystal",
|
||||
|
||||
"It is widely accepted that mana cannot be created or destroyed, and that when a spell is cast, the mana it channels is simply dissipated into the surroundings. Indeed, this is how the vast majority of mana exists - spread thinly throughout the world. However, to be of any use, it must be concentrated, either naturally over thousands of years, as is the case with crystals underground, or artificially - an advanced subject covered later in this book. There also exist various ways of making spells more mana-efficient and recovering some of the mana dissipated during spellcasting."
|
||||
]
|
||||
},
|
||||
|
||||
"wands": {
|
||||
"title": "Wands",
|
||||
"include_in_contents": "main_contents",
|
||||
"triggers": [
|
||||
"ebwizardry:arcane_initiate"
|
||||
],
|
||||
"text": [
|
||||
|
||||
"The wand is the implement of choice for a wizard. With it, you can cast any @spells spell@ provided the wand can contain its power (see @tiers Tiers@). Wands come in many different varieties but you will almost certainly start with a basic magic wand.",
|
||||
|
||||
"#image magic_wand",
|
||||
|
||||
"Most wands begin their life as a simple arrangement of a magic crystal of some sort attached to a wooden stick, with a gold nugget affixed to the other end. The crystal is, of course, the source of the wand's power, as it provides the focus necessary to channel @spells@. The rest of the wand is important too though, as it provides not only a means with which to hold the wand, but also a path through which @mana@ can be channelled and directed.",
|
||||
|
||||
"As more spells are cast with a wand, it grows more effective at channelling @spells@, and can therefore cast spells of greater power. This effect can be discerned from subtle changes that occur in the shape and appearance of the wand itself: as a wand grows more powerful, its crystal will become more vibrant, it may change in colour depending on its element, and after a while, the wood from which it is made will start to grow and twist into more complex shapes.",
|
||||
|
||||
"When holding a wand, a small heads-up display will show in the corner of the screen. This indicates the spell that is currently selected along with a pictorial representation of it, and, if the spell has been cast, a cooldown bar indicating the time until that spell can be cast again. It also shows the names of the next and previous spells bound to the wand. To switch between spells, use the #next_spell_key and #previous_spell_key keys (these can be changed in options -> controls). You can also switch spells by scrolling the mouse wheel while sneaking.",
|
||||
|
||||
"When viewing an inventory, hovering over a wand will display how much @mana@ is stored in it, along with its currently selected @spells spell@ and any specific abilities it may have. More in-depth information about a wand can be viewed by placing it in an @arcane_workbench arcane workbench@."
|
||||
]
|
||||
},
|
||||
|
||||
"spells": {
|
||||
"title": "Spells",
|
||||
"include_in_contents": "main_contents",
|
||||
"triggers": [
|
||||
"ebwizardry:handbook/spells"
|
||||
],
|
||||
"text": [
|
||||
|
||||
"A @wands wand@ is useless without spells to cast with it. Spells are recorded in two forms: in books, which are permanent, and in scrolls, which are temporary.",
|
||||
|
||||
"Spell books are of little use on their own; instead they are used to bind the spell they contain to @wands@. Spell books can be found throughout the world and it will not take long for you to come across one. When you do find one, you can mouse over the spell book to see basic information about the spell at a glance. You can also right-click whilst holding a spell book to read more detailed information about the spell.",
|
||||
|
||||
"Scrolls, on the other hand, serve a different purpose: right-clicking whilst holding one will cast the spell that is bound to it, destroying the scroll in the process. Like spell books, scrolls can be found throughout the world, but you can also make them yourself by crafting a blank scroll from a piece of paper and some string. You can then bind a spell to the scroll using the @arcane_workbench arcane workbench@, but only if you have knowledge of that spell (see @enchanting_scrolls Enchanting Scrolls@). Scrolls are quite useful when you need to use a spell once or twice on a particular quest but don't want it to take up a slot on your @wands wand@. It should be noted, however, that scrolls are at best an inefficient method of casting spells.",
|
||||
|
||||
"To the untrained eye, the magical runes with which spells are written are unreadable. In order to identify a spell, you could, of course, cast it and see what happens - though doing so may cause unwanted side-effects, and many spells need certain conditions in order to work. A safer and more reliable option is to use a scroll of identification, which can be found in chests or purchased from a wizard. Right-clicking whilst holding one will identify the first unknown spell book or scroll on your hotbar, consuming the scroll of identification in the process."
|
||||
]
|
||||
},
|
||||
|
||||
"arcane_workbench": {
|
||||
"title": "Arcane Workbench",
|
||||
"include_in_contents": "main_contents",
|
||||
"contents": {
|
||||
"id": "arcane_workbench_subsections",
|
||||
"hyperlinks": true,
|
||||
"page_numbers": true,
|
||||
"separator": "."
|
||||
},
|
||||
"triggers": [
|
||||
"ebwizardry:handbook/arcane_workbench"
|
||||
],
|
||||
"text": [
|
||||
|
||||
"The arcane workbench is a block similar in appearance to the enchantment table where you can charge, upgrade, and bind @spells@ to your @wands wand@. Simply place it anywhere and right click, and you will see something like this:",
|
||||
|
||||
"#image workbench"
|
||||
],
|
||||
"sections": {
|
||||
"binding_spells": {
|
||||
"title": "Binding Spells",
|
||||
"include_in_contents": "arcane_workbench_subsections",
|
||||
"text": [
|
||||
|
||||
"To bind a spell to your wand, simply place your wand in the central slot of the workbench, then place the spell book in one of the surrounding slots and press apply. You will notice that the spell book is left untouched during this process - this is because the act of spell binding does not 'take' the spell from the spell book; rather, it attunes the wand to the spell. As such, you may change the spells bound to your wand as much as you wish by simply repeating the spell binding process. You can bind up to five spells to a wand at one time, though this number may be increased with attunement upgrades."
|
||||
]
|
||||
},
|
||||
"charging_wands": {
|
||||
"title": "Charging Wands",
|
||||
"include_in_contents": "arcane_workbench_subsections",
|
||||
"text": [
|
||||
|
||||
"To charge your wand, place the wand in the central slot of the arcane workbench and place some magic crystals in the upper of the two slots on the left, and then press apply. Each crystal is worth #mana_per_crystal mana, and the wand will only take what it needs, so you can keep a supply of crystals in the workbench for when you need them. However, bear in mind that the wand can only take a whole number of crystals so if the wand needs, for example, 30 more mana, #mana_per_crystal_minus_30 mana will be lost when charging it."
|
||||
]
|
||||
},
|
||||
"upgrading_wands": {
|
||||
"title": "Upgrading Wands",
|
||||
"include_in_contents": "arcane_workbench_subsections",
|
||||
"text": [
|
||||
|
||||
"To upgrade your wand, you will need a tome of arcana of the appropriate tier (see @tiers Tiers@) or a special wand upgrade. Both of these can be found as loot or purchased from a wizard. To upgrade your wand, place it in the central slot and the upgrade in the lower of the two slots on the left, then press apply.",
|
||||
|
||||
"Special wand upgrades improve a particular aspect of a wand. Each type of upgrade can stack up to three times, and the total number of upgrades that a wand can take depends on its tier. Upgrades cannot be removed."
|
||||
]
|
||||
},
|
||||
"enchanting_scrolls": {
|
||||
"title": "Enchanting Scrolls",
|
||||
"include_in_contents": "arcane_workbench_subsections",
|
||||
"text": [
|
||||
|
||||
"The arcane workbench can also be used to enchant spell scrolls. To do so, you will require a blank scroll, crafted from a piece of paper and some string, a spell book for your chosen spell, and enough magic crystals to provide mana to cast it. Place the crystals in the upper left-hand slot, the blank scroll in the central slot, and the spell book in the single slot that appears above it, then press apply to enchant the scroll."
|
||||
]
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
"wizard_armour": {
|
||||
"title": "Wizard Armour",
|
||||
"include_in_contents": "main_contents",
|
||||
"text": [
|
||||
|
||||
"As a wizard, you will need something to protect you from the creatures you fight. No ordinary armour will do though. To make the most of your spells, you will need wizard armour: hat, robes, leggings and boots. Unlike ordinary armour, these will not break. Instead, they use arcane energy to shield the wearer. This of course means that they must be charged with magic crystals, just like wands. Fortunately, these garments do not consume much mana. Charge them as you would a wand in the arcane workbench or with a mana flask, but be careful - if they run out of mana, you will find yourself defenceless.",
|
||||
|
||||
"You can obtain wizard armour by crafting it from magical silk, obtained by crafting string with a magic crystal.",
|
||||
|
||||
"Those wizards who devote themselves to the practice of one element sometimes wear special garments which grant bonuses for that element. Full sets of such garments are very sought after among wizards and are not easy to find.",
|
||||
|
||||
"Legend speaks of an arcane seal used by the most powerful wizards to greatly improve their armour."
|
||||
]
|
||||
},
|
||||
|
||||
"magical_world": {
|
||||
"title": "Magic in the World",
|
||||
"include_in_contents": "main_contents",
|
||||
"contents": {
|
||||
"id": "magical_world_subsections",
|
||||
"hyperlinks": true,
|
||||
"page_numbers": true,
|
||||
"separator": "."
|
||||
},
|
||||
"text": [
|
||||
|
||||
"One cannot master the arcane just by staying at home - there is a world full of magic out there to explore! Ancient ruins, relics and mysterious beings good and bad await discovery - if you know where to look. The greatest wizards are also the most curious, and gain much of their knowledge and power through exploration of the environment around them, and experimenting with what they find.",
|
||||
|
||||
"Whilst much of a wizard's time is inevitably spent alone, the sharing of knowledge is also vital to learning the arcane arts. Seek out fellow practicioners of magic, and learn as much as you can from them: communication is arguably the greatest power of all."
|
||||
],
|
||||
"sections": {
|
||||
"crystal_flowers": {
|
||||
"title": "Crystal Flowers",
|
||||
"include_in_contents": "magical_world_subsections",
|
||||
"triggers": [
|
||||
"ebwizardry:handbook/crystal_flowers"
|
||||
],
|
||||
"text": [
|
||||
|
||||
"During your travels, you will probably come across curious glowing flowers growing in the wild from time to time. These distinctive blooms are known as crystal flowers, and they are strangely effective at concentrating mana - to this day, nobody is quite sure why. We do know, however, that they can be harvested and crafted to extract the mana as crystals, making them a rather useful above-ground source of mana. The amount of mana obtainable this way is limited, though, by the small size of the flowers and due to them only growing in small patches.",
|
||||
|
||||
"#image crystal_flower"
|
||||
]
|
||||
},
|
||||
"wizard_towers": {
|
||||
"title": "Wizard Towers",
|
||||
"include_in_contents": "magical_world_subsections",
|
||||
"text": [
|
||||
|
||||
"Fear not - you are not the only practicioner of magic in this world. On your travels, you may well encounter a tall tower or two with a distinctive pointed roof. This is the residence of a fellow wizard. A life of solitude can make wizards a little grumpy at times, but they are usually friendly folk who are willing to share their knowledge with you - albeit for a price. Expect to pay precious metals and gems, and in return you will recieve many a great arcane wonder. If, however, it is a master spell you seek, you will need to speak to a specialist.",
|
||||
|
||||
"Unfortunately, there are a few wizards out there who do not welcome visitors. These wizards are typically outcasts, and are hostile to anyone crossing their path. Approach these individuals with caution, and be prepared to defend yourself against powerful magic. Defeat them however, and their knowledge is yours for the taking.",
|
||||
|
||||
"It should also be noted that no wizard will take kindly to being attacked or stolen from - and will readily take the law into their own hands."
|
||||
]
|
||||
},
|
||||
"obelisks": {
|
||||
"title": "Obelisks",
|
||||
"include_in_contents": "magical_world_subsections",
|
||||
"text": [
|
||||
|
||||
"Vestiges of ancient magic are scattered throughout the world, the most notable of which are carved stone structures which bear a great many symbols and runes. These ruins are all that remain of what appears to have been some kind of ancient civilisation. Whoever, or whatever, built them, they clearly had a considerable knowledge of magic, and used it to place protective enchantments over such locations that still persist to this day.",
|
||||
|
||||
"These stuctures fit broadly into two types. The first, and more common, of these are obelisks: tall spikes of carved stone, known as runestone, with an open structure at the bottom that usually contains minor arcane relics from the forgotten past. These structures are typically protected by an enchantment that summons hostile magical creatures should any human stray too close. Fend off these creatures and destroy their source, and the relics are yours."
|
||||
]
|
||||
},
|
||||
"shrines": {
|
||||
"title": "Shrines",
|
||||
"include_in_contents": "magical_world_subsections",
|
||||
"text": [
|
||||
|
||||
"The second, and rarer, kind of structure is known as a shrine. These structures consist of a circle of runestone pillars surrounding a central platform, upon which sits a chest filled with ancient artefacts. This chest is typically protected by an arcane lock enchantment, preventing anyone except the owner from opening it. The entire structure is also protected by a containment field, which prevents anything that strays too close from escaping.",
|
||||
|
||||
"Due to their great arcane power and significance, not to mention the riches within, these structures are particularly attractive to any aspiring wizard - but be cautious. There are numerous reports of wizards becoming trapped within containment fields and slowly being driven insane, perhaps by claustrophobia. A growing number, however, believe such occurences to be a deliberate part of the shrine's protective magic, and that the wizards trapped within are in fact being controlled to protect the structure. A chilling prospect, surely, for anyone who dares to venture near..."
|
||||
]
|
||||
},
|
||||
"creatures": {
|
||||
"title": "Magical Creatures",
|
||||
"include_in_contents": "magical_world_subsections",
|
||||
"text": [
|
||||
|
||||
"Besides wizards, a multitude of other creatures also use magic. Many of these creatures are arcane in origin, and most can be summoned using certain magical spells. One may encounter these creatures guarding an obelisk, or perhaps a powerful summoner. Lesser arcane beings are even found occasionally in the wilderness."
|
||||
]
|
||||
},
|
||||
"artefacts": {
|
||||
"title": "Artefacts",
|
||||
"include_in_contents": "magical_world_subsections",
|
||||
"text": [
|
||||
|
||||
"When exploring a shrine, you may be lucky enough to uncover some kind of ancient magical artefact - an object capable of granting its wearer unique and powerful buffs and special powers. Three types are known to have been found: rings, which grant bonuses and effects to spells, amulets, which improve defensive abilities, and trinkets, which give utility effects. These artefacts appear to function only when worn appropriately; simply having them on one's person is insufficient."
|
||||
]
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
"tiers": {
|
||||
"title": "Tiers",
|
||||
"include_in_contents": "main_contents",
|
||||
"text":[
|
||||
|
||||
"Wands and spells come in four tiers: #colour_novicenovice#colour_reset, #colour_apprenticeapprentice#colour_reset, #colour_advancedadvanced #colour_resetand #colour_mastermaster#colour_reset. Each tier is more powerful than the last.",
|
||||
|
||||
"#colour_noviceNovice #colour_resetwands are the ones that can be crafted. They hold up to #novice_max_charge mana, and can only cast novice spells. Novice spells are simple enough to be cast by anyone and they do not usually cost much mana. This does not necessarily mean that they are useless at higher tiers however; their low cost makes some novice spells useful even when you have access to master spells.",
|
||||
|
||||
"#colour_apprenticeApprentice #colour_resetwands are the next tier up from novice wands. They hold up to #apprentice_max_charge mana and can cast novice and apprentice spells. Apprentice spells are a little more impressive than novice spells but usually cost more.",
|
||||
|
||||
"#colour_advancedAdvanced #colour_resetwands are quite rare and are much more powerful. They can cast all spells except master spells, and hold #advanced_max_charge mana. Advanced spells can grant superhuman powers such as invisibility and wreak havoc on enemies.",
|
||||
|
||||
"#colour_masterMaster #colour_resetwands are the most powerful wands in existence. They hold up to #master_max_charge mana, and can cast any spell. Master spell books are very rare and the spells can cause complete devastation not only to enemies but even the world itself. Use with caution."
|
||||
]
|
||||
},
|
||||
|
||||
"elements": {
|
||||
"title": "Elements",
|
||||
"include_in_contents": "main_contents",
|
||||
"text": [
|
||||
|
||||
"Spells belong to different elements, which describe the nature of the spell and also have perks when used with certain wands.",
|
||||
|
||||
"#colour_fireFire#colour_reset \nPerhaps the most destructive element, fire concerns burning, lava, and explosions. A powerful pyromancer will unleash hell upon their enemies and probably set the world alight in the process. Most fire attack spells set fire to their target, causing ongoing damage over time. However, be wary that fire spells may have no effect on nether mobs.",
|
||||
|
||||
"#colour_iceIce#colour_reset \nThe element of ice is made of all that is cold. Frost spells often slow enemies down or freeze them completely, and are especially effective on creatures of fire. Frost magic can prove equally useful outside of combat, such as freezing water to cross a river.",
|
||||
|
||||
"#colour_lightningLightning#colour_reset \nThis element concerns lightning, storms, and the weather. A powerful storm mage is a force to be reckoned with, and some posess the ability to call down lightning at will. Lightning spells often damage multiple enemies at once and usually seek their target, making them an effective attack against any mob... but beware of creepers.",
|
||||
|
||||
"#colour_necromancyNecromancy#colour_reset \nNecromancy is the element of darkness, chaos and the undead. Necromancers are mysterious and often regarded as evil, though this is usually not the case. Necromancy spells are commonly used to summon creatures to fight for you or even bend the will of your enemies.",
|
||||
|
||||
"#colour_earthEarth#colour_reset \nThe element of earth is focused on the natural world: animals, plants, the wind, and such like. Earth magic is diverse and takes a wide variety of forms, from poisoning enemies to unleashing the fury of the weather. Earth spells are a mixture of attack, defence and utility.",
|
||||
|
||||
"#colour_sorcerySorcery#colour_reset \nSorcery is the element of force and change. Sorcerers manipulate light, gravity and even reality itself to fit their needs. Sorcery spells can be used, among other things, to grant their caster magical powers or move objects to their will.",
|
||||
|
||||
"#colour_healingHealing#colour_reset \nThe element of healing is concerned with defence and regeneration. Healers seek to protect themselves and their allies as much as possible and as such can be very difficult opponents. Though generally not used as an attack, the purifying light of some healing spells will do considerable damage to the undead. Healing spells are indispensable when in combat.",
|
||||
|
||||
"The boundaries between the different elements are not always distinct, and some spells bear traits of elements other than their own.",
|
||||
|
||||
"If you are lucky, you may happen upon an elemental wand. These wands will allow you to cast spells of the right element for more potency than usual."
|
||||
]
|
||||
},
|
||||
|
||||
"growing_crystals": {
|
||||
"title": "Growing Crystals",
|
||||
"include_in_contents": "main_contents",
|
||||
"text": [
|
||||
|
||||
"Magic crystals normally form naturally underground over thousands of years, by gradually drawing latent mana from the surrounding rock as they grow. Crystalline structures are very good at absorbing and holding mana, hence the mana is naturally drawn towards them. Given the right conditions, however, it is possible to greatly speed up this process. This has given rise to the advanced technique of crystal growing, which requires both great patience and attention to detail.",
|
||||
|
||||
"In order to encourage crystal growth, one must begin with a crystal seed. The most effective seeds have proven to be crystal shards, obtained by simply shattering a magic crystal into pieces. A crystal seed must then be planted into some kind of substrate, usually rock, although other materials are also viable. Left alone, this would produce a sizeable crystal in hundreds of years, depending on the conditions.",
|
||||
|
||||
"To reduce this length of time, several things can be done. Immersing the seed and substrate in mineral-rich water helps with crystal growth, but not with concentrating mana. To concentrate mana more quickly, two techniques can be employed: firstly, surrounding the growth site with lesser mana-gathering objects allows the crystal to draw from them, meaning the surrounding environment is constantly replenished with mana. Too many, however, and they begin to draw mana away from the growing crystal. Secondly, mana can be drawn through some materials better than others: air is very poor at this, whereas dense materials tend to be much better. Some wizards have taken to experimentation with exotic, magical, or even otherworldy materials to try and improve crystal growth. Some even claim to have altered the type of crystal that is formed in this way.",
|
||||
|
||||
"If conditions are exactly right, it is even possible for crystals to grow far beyond their natural size. Such crystals are known as grand magic crystals, and besides containing a great quantity of mana, they have a few specific uses."
|
||||
]
|
||||
},
|
||||
|
||||
"miscellaneous": {
|
||||
"title": "Miscellaneous",
|
||||
"include_in_contents": "main_contents",
|
||||
"contents": {
|
||||
"id": "miscellaneous_subsections",
|
||||
"hyperlinks": true,
|
||||
"page_numbers": true,
|
||||
"separator": "."
|
||||
},
|
||||
"sections": {
|
||||
"mana_flasks": {
|
||||
"title": "Mana Flasks",
|
||||
"include_in_contents": "miscellaneous_subsections",
|
||||
"text": [
|
||||
|
||||
"Arkendur's Arcane Supplies Co. - magical items for your every need!",
|
||||
|
||||
"Need to recharge your wand on the go? No problem! Mana can now be bottled.** Simply craft a mana flask with a glass bottle and eight magic crystals and take it with you. When you need to use it, simply craft it with your wand and it will restore some of the charge.*",
|
||||
|
||||
"NEW! Introducing the brand-new large*** and small mana flasks - now you can choose a size of mana flask to meet your needs!",
|
||||
|
||||
"* This process will consume the bottle.",
|
||||
"** Some mana is lost during bottling.",
|
||||
"*** Large mana flask requires grand magic crystals."
|
||||
]
|
||||
},
|
||||
"throwable_items": {
|
||||
"title": "Throwable Items",
|
||||
"include_in_contents": "miscellaneous_subsections",
|
||||
"text": [
|
||||
|
||||
"Various spells conjure physical items, some of which can also be crafted directly. Firebombs, poison bombs, spark bombs and smoke bombs can all be crafted."
|
||||
]
|
||||
},
|
||||
"automated_casting": {
|
||||
"title": "Automated Casting",
|
||||
"include_in_contents": "miscellaneous_subsections",
|
||||
"text": [
|
||||
|
||||
"Recent experimentation has revealed that it is possible to automate spell casting, to a degree, using nothing more than a simple dispenser. Nobody is quite sure why, but it would appear that the strange properties of redstone even extend to triggering the activation of spell scrolls. Placing a few into a dispenser and powering it ought to do the trick..."
|
||||
]
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
"crafting_recipes": {
|
||||
"title": "Crafting Recipes",
|
||||
"include_in_contents": "main_contents",
|
||||
"text": [
|
||||
|
||||
"#recipe arcane_workbench",
|
||||
"#recipe magic_wand",
|
||||
"#recipe magic_missile_spell_book",
|
||||
"#recipe wizard_handbook",
|
||||
"#recipe crystal_flower_to_crystals",
|
||||
"#recipe mana_flask",
|
||||
"#recipe transportation_stone",
|
||||
"#recipe magic_silk",
|
||||
"#recipe wizard_hat",
|
||||
"#recipe wizard_robe",
|
||||
"#recipe wizard_leggings",
|
||||
"#recipe wizard_boots",
|
||||
"#recipe blank_scroll",
|
||||
"#recipe firebomb",
|
||||
"#recipe poison_bomb",
|
||||
"#recipe smoke_bomb"
|
||||
]
|
||||
},
|
||||
|
||||
"credits": {
|
||||
"title": "Credits",
|
||||
"include_in_contents": "main_contents",
|
||||
"text": [
|
||||
|
||||
"Electroblob's Wizardry \nVersion #version \nFor Minecraft #mcversion",
|
||||
|
||||
"For more information, check out the @https://github.com/Electroblob77/Wizardry/wiki wiki@.",
|
||||
|
||||
"Designed, coded and textured by Electroblob",
|
||||
|
||||
"Thanks to Minecraft Forge and MCP, without which this mod would not have been possible.",
|
||||
|
||||
"Thanks also to the Minecraft modding community, which always has an answer to my modding problems!",
|
||||
|
||||
"In addition, I'd like to thank the following individuals for their contributions to the mod:",
|
||||
|
||||
"Code:",
|
||||
|
||||
"- Corail31 \n- 12foo \n- Shadows-of-Fire",
|
||||
|
||||
"Translations:",
|
||||
|
||||
"- Russian: VilagVil \n- Spanish and Mexican Spanish: MadWrist \n- Chinese: ZHENGLOC",
|
||||
|
||||
"Lightning ray sound effect from OhhWowProductions"
|
||||
]
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,528 @@
|
||||
{
|
||||
"ruler_text": "--------------------",
|
||||
|
||||
"colours": {
|
||||
"text": "#000000",
|
||||
"caption": "#666666",
|
||||
"hyperlink": "#601ba0",
|
||||
"highlight": "#dd4c1d"
|
||||
},
|
||||
|
||||
"images": {
|
||||
"workbench": {
|
||||
"location": "ebwizardry:textures/gui/arcane_workbench_picture.png",
|
||||
"caption": "The Arcane Workbench",
|
||||
"u": 0,
|
||||
"v": 0,
|
||||
"width": 110,
|
||||
"height": 110
|
||||
},
|
||||
"crystal_ore": {
|
||||
"location": "ebwizardry:textures/gui/ore_picture.png",
|
||||
"caption": "Crystal Ore",
|
||||
"u": 0,
|
||||
"v": 0,
|
||||
"width": 64,
|
||||
"height": 64
|
||||
},
|
||||
"magic_crystal": {
|
||||
"location": "ebwizardry:textures/items/crystal_magic.png",
|
||||
"caption": "A Magic Crystal",
|
||||
"u": 6,
|
||||
"v": 6,
|
||||
"width": 36,
|
||||
"height": 33,
|
||||
"texture_width": 48,
|
||||
"texture_height": 48
|
||||
},
|
||||
"magic_wand": {
|
||||
"location": "ebwizardry:textures/items/wand_basic.png",
|
||||
"caption": "A Magic Wand",
|
||||
"u": 0,
|
||||
"v": 0,
|
||||
"width": 64,
|
||||
"height": 64
|
||||
},
|
||||
"crystal_flower": {
|
||||
"location": "ebwizardry:textures/gui/flower_picture.png",
|
||||
"caption": "A Crystal Flower",
|
||||
"u": 0,
|
||||
"v": 0,
|
||||
"width": 64,
|
||||
"height": 64
|
||||
}
|
||||
},
|
||||
|
||||
"recipes": {
|
||||
"arcane_workbench": {
|
||||
"location": "ebwizardry:arcane_workbench"
|
||||
},
|
||||
"magic_wand": {
|
||||
"location": "ebwizardry:magic_wand"
|
||||
},
|
||||
"magic_missile_spell_book": {
|
||||
"location": "ebwizardry:magic_missile_spell_book"
|
||||
},
|
||||
"wizard_handbook": {
|
||||
"location": "ebwizardry:wizard_handbook"
|
||||
},
|
||||
"crystal_flower_to_crystals": {
|
||||
"location": "ebwizardry:crystal_flower_to_crystals"
|
||||
},
|
||||
"mana_flask": {
|
||||
"location": "ebwizardry:mana_flask"
|
||||
},
|
||||
"transportation_stone": {
|
||||
"location": "ebwizardry:transportation_stone"
|
||||
},
|
||||
"magic_silk": {
|
||||
"location": "ebwizardry:magic_silk"
|
||||
},
|
||||
"wizard_hat": {
|
||||
"location": "ebwizardry:wizard_hat"
|
||||
},
|
||||
"wizard_robe": {
|
||||
"location": "ebwizardry:wizard_robe"
|
||||
},
|
||||
"wizard_leggings": {
|
||||
"location": "ebwizardry:wizard_leggings"
|
||||
},
|
||||
"wizard_boots": {
|
||||
"location": "ebwizardry:wizard_boots"
|
||||
},
|
||||
"blank_scroll": {
|
||||
"location": "ebwizardry:blank_scroll"
|
||||
},
|
||||
"firebomb": {
|
||||
"location": "ebwizardry:firebomb"
|
||||
},
|
||||
"poison_bomb": {
|
||||
"location": "ebwizardry:poison_bomb"
|
||||
},
|
||||
"smoke_bomb": {
|
||||
"location": "ebwizardry:smoke_bomb"
|
||||
}
|
||||
},
|
||||
|
||||
"sections": {
|
||||
|
||||
"inside_cover": {
|
||||
"centre": {
|
||||
"x": true,
|
||||
"y": true
|
||||
},
|
||||
"text": [
|
||||
|
||||
"The Wizard's Handbook",
|
||||
|
||||
"By Electroblob"
|
||||
]
|
||||
},
|
||||
|
||||
"introduction": {
|
||||
"title": "Introduction",
|
||||
"text": [
|
||||
|
||||
"Greetings, wizard! This book explains the many ways of the arcane and how to use them. This is no ordinary book, though - its pages will materialize before you as you discover more about the magical world.",
|
||||
|
||||
"Use the arrow buttons to turn between pages, and the double-arrow buttons to quickly flip between sections. Use the central menu button to return to the main contents page.",
|
||||
|
||||
"Click on any purple link text to jump straight to the relevant page. Right-click the bookmark to move it to the current page, and left-click to return to the bookmarked page."
|
||||
]
|
||||
},
|
||||
|
||||
"main_contents": {
|
||||
"title": "Contents",
|
||||
"contents": {
|
||||
"id": "main_contents",
|
||||
"hyperlinks": true,
|
||||
"page_numbers": true,
|
||||
"separator": "."
|
||||
}
|
||||
},
|
||||
|
||||
"setting_up": {
|
||||
"title": "Setting Up",
|
||||
"include_in_contents": "main_contents",
|
||||
"text": [
|
||||
|
||||
"To get started with wizardry, you will need a few things:",
|
||||
|
||||
"- A magic wand, crafted with a gold nugget, a stick, and a magic crystal.",
|
||||
|
||||
"- An @arcane_workbench arcane workbench@, crafted with 3 stone, 1 lapis lazuli block, 2 magic crystals, 2 gold nuggets and 1 purple carpet.",
|
||||
|
||||
"- A @spells spell@ book. These can be found in chests, dropped as loot, purchased from wizards, or you can make a beginner's spell, magic missile, with a book and 4 magic crystals.",
|
||||
|
||||
"You will also need a bunch more magic crystals to supply your wand with @mana@."
|
||||
]
|
||||
},
|
||||
|
||||
"mana": {
|
||||
"title": "Mana",
|
||||
"include_in_contents": "main_contents",
|
||||
"triggers": [
|
||||
"ebwizardry:crystal"
|
||||
],
|
||||
"text": [
|
||||
|
||||
"Mana is the arcane energy that gives wizards their powers. It is not a physical substance in its own right; rather, it is an everpresent aura which permeates everything in the world. In particular, it manifests itself in naturally occurring crystals, which can be found embedded in rocks underground. @wands Wands@ can store mana within them, and this mana is channeled into the @spells@ that you cast. Different spells require different amounts of mana, depending on how powerful they are and how long they last.",
|
||||
|
||||
"#image crystal_ore",
|
||||
|
||||
"#image magic_crystal",
|
||||
|
||||
"It is widely accepted that mana cannot be created or destroyed, and that when a spell is cast, the mana it channels is simply dissipated into the surroundings. Indeed, this is how the vast majority of mana exists - spread thinly throughout the world. However, to be of any use, it must be concentrated, either naturally over thousands of years, as is the case with crystals underground, or artificially - an advanced subject covered later in this book. There also exist various ways of making spells more mana-efficient and recovering some of the mana dissipated during spellcasting."
|
||||
]
|
||||
},
|
||||
|
||||
"wands": {
|
||||
"title": "Wands",
|
||||
"include_in_contents": "main_contents",
|
||||
"triggers": [
|
||||
"ebwizardry:arcane_initiate"
|
||||
],
|
||||
"text": [
|
||||
|
||||
"The wand is the implement of choice for a wizard. With it, you can cast any @spells spell@ provided the wand can contain its power (see @tiers Tiers@). Wands come in many different varieties but you will almost certainly start with a basic magic wand.",
|
||||
|
||||
"#image magic_wand",
|
||||
|
||||
"Most wands begin their life as a simple arrangement of a magic crystal of some sort attached to a wooden stick, with a gold nugget affixed to the other end. The crystal is, of course, the source of the wand's power, as it provides the focus necessary to channel @spells@. The rest of the wand is important too though, as it provides not only a means with which to hold the wand, but also a path through which @mana@ can be channeled and directed.",
|
||||
|
||||
"As more spells are cast with a wand, it grows more effective at channeling @spells@, and can therefore cast spells of greater power. This effect can be discerned from subtle changes that occur in the shape and appearance of the wand itself: as a wand grows more powerful, its crystal will become more vibrant, it may change in color depending on its element, and after a while, the wood from which it is made will start to grow and twist into more complex shapes.",
|
||||
|
||||
"When holding a wand, a small heads-up display will show in the corner of the screen. This indicates the spell that is currently selected along with a pictorial representation of it, and, if the spell has been cast, a cooldown bar indicating the time until that spell can be cast again. It also shows the names of the next and previous spells bound to the wand. To switch between spells, use the #next_spell_key and #previous_spell_key keys (these can be changed in options -> controls). You can also switch spells by scrolling the mouse wheel while sneaking.",
|
||||
|
||||
"When viewing an inventory, hovering over a wand will display how much @mana@ is stored in it, along with its currently selected @spells spell@ and any specific abilities it may have. More in-depth information about a wand can be viewed by placing it in an @arcane_workbench arcane workbench@."
|
||||
]
|
||||
},
|
||||
|
||||
"spells": {
|
||||
"title": "Spells",
|
||||
"include_in_contents": "main_contents",
|
||||
"triggers": [
|
||||
"ebwizardry:handbook/spells"
|
||||
],
|
||||
"text": [
|
||||
|
||||
"A @wands wand@ is useless without spells to cast with it. Spells are recorded in two forms: in books, which are permanent, and in scrolls, which are temporary.",
|
||||
|
||||
"Spell books are of little use on their own; instead they are used to bind the spell they contain to @wands@. Spell books can be found throughout the world and it will not take long for you to come across one. When you do find one, you can mouse over the spell book to see basic information about the spell at a glance. You can also right-click whilst holding a spell book to read more detailed information about the spell.",
|
||||
|
||||
"Scrolls, on the other hand, serve a different purpose: right-clicking whilst holding one will cast the spell that is bound to it, destroying the scroll in the process. Like spell books, scrolls can be found throughout the world, but you can also make them yourself by crafting a blank scroll from a piece of paper and some string. You can then bind a spell to the scroll using the @arcane_workbench arcane workbench@, but only if you have knowledge of that spell (see @enchanting_scrolls Enchanting Scrolls@). Scrolls are quite useful when you need to use a spell once or twice on a particular quest but don't want it to take up a slot on your @wands wand@. It should be noted, however, that scrolls are at best an inefficient method of casting spells.",
|
||||
|
||||
"To the untrained eye, the magical runes with which spells are written are unreadable. In order to identify a spell, you could, of course, cast it and see what happens - though doing so may cause unwanted side-effects, and many spells need certain conditions in order to work. A safer and more reliable option is to use a scroll of identification, which can be found in chests or purchased from a wizard. Right-clicking whilst holding one will identify the first unknown spell book or scroll on your hotbar, consuming the scroll of identification in the process."
|
||||
]
|
||||
},
|
||||
|
||||
"arcane_workbench": {
|
||||
"title": "Arcane Workbench",
|
||||
"include_in_contents": "main_contents",
|
||||
"contents": {
|
||||
"id": "arcane_workbench_subsections",
|
||||
"hyperlinks": true,
|
||||
"page_numbers": true,
|
||||
"separator": "."
|
||||
},
|
||||
"triggers": [
|
||||
"ebwizardry:handbook/arcane_workbench"
|
||||
],
|
||||
"text": [
|
||||
|
||||
"The arcane workbench is a block similar in appearance to the enchantment table where you can charge, upgrade, and bind @spells@ to your @wands wand@. Simply place it anywhere and right click, and you will see something like this:",
|
||||
|
||||
"#image workbench"
|
||||
],
|
||||
"sections": {
|
||||
"binding_spells": {
|
||||
"title": "Binding Spells",
|
||||
"include_in_contents": "arcane_workbench_subsections",
|
||||
"text": [
|
||||
|
||||
"To bind a spell to your wand, simply place your wand in the central slot of the workbench, then place the spell book in one of the surrounding slots and press apply. You will notice that the spell book is left untouched during this process - this is because the act of spell binding does not 'take' the spell from the spell book; rather, it attunes the wand to the spell. As such, you may change the spells bound to your wand as much as you wish by simply repeating the spell binding process. You can bind up to five spells to a wand at one time, though this number may be increased with attunement upgrades."
|
||||
]
|
||||
},
|
||||
"charging_wands": {
|
||||
"title": "Charging Wands",
|
||||
"include_in_contents": "arcane_workbench_subsections",
|
||||
"text": [
|
||||
|
||||
"To charge your wand, place the wand in the central slot of the arcane workbench and place some magic crystals in the upper of the two slots on the left, and then press apply. Each crystal is worth #mana_per_crystal mana, and the wand will only take what it needs, so you can keep a supply of crystals in the workbench for when you need them. However, bear in mind that the wand can only take a whole number of crystals so if the wand needs, for example, 30 more mana, #mana_per_crystal_minus_30 mana will be lost when charging it."
|
||||
]
|
||||
},
|
||||
"upgrading_wands": {
|
||||
"title": "Upgrading Wands",
|
||||
"include_in_contents": "arcane_workbench_subsections",
|
||||
"text": [
|
||||
|
||||
"To upgrade your wand, you will need a tome of arcana of the appropriate tier (see @tiers Tiers@) or a special wand upgrade. Both of these can be found as loot or purchased from a wizard. To upgrade your wand, place it in the central slot and the upgrade in the lower of the two slots on the left, then press apply.",
|
||||
|
||||
"Special wand upgrades improve a particular aspect of a wand. Each type of upgrade can stack up to three times, and the total number of upgrades that a wand can take depends on its tier. Upgrades cannot be removed."
|
||||
]
|
||||
},
|
||||
"enchanting_scrolls": {
|
||||
"title": "Enchanting Scrolls",
|
||||
"include_in_contents": "arcane_workbench_subsections",
|
||||
"text": [
|
||||
|
||||
"The arcane workbench can also be used to enchant spell scrolls. To do so, you will require a blank scroll, crafted from a piece of paper and some string, a spell book for your chosen spell, and enough magic crystals to provide mana to cast it. Place the crystals in the upper left-hand slot, the blank scroll in the central slot, and the spell book in the single slot that appears above it, then press apply to enchant the scroll."
|
||||
]
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
"wizard_armour": {
|
||||
"title": "Wizard Armor",
|
||||
"include_in_contents": "main_contents",
|
||||
"text": [
|
||||
|
||||
"As a wizard, you will need something to protect you from the creatures you fight. No ordinary armor will do though. To make the most of your spells, you will need wizard armor: hat, robes, leggings and boots. Unlike ordinary armor, these will not break. Instead, they use arcane energy to shield the wearer. This of course means that they must be charged with magic crystals, just like wands. Fortunately, these garments do not consume much mana. Charge them as you would a wand in the arcane workbench or with a mana flask, but be careful - if they run out of mana, you will find yourself defenseless.",
|
||||
|
||||
"You can obtain wizard armor by crafting it from magical silk, obtained by crafting string with a magic crystal.",
|
||||
|
||||
"Those wizards who devote themselves to the practise of one element sometimes wear special garments which grant bonuses for that element. Full sets of such garments are very sought after among wizards and are not easy to find.",
|
||||
|
||||
"Legend speaks of an arcane seal used by the most powerful wizards to greatly improve their armor."
|
||||
]
|
||||
},
|
||||
|
||||
"magical_world": {
|
||||
"title": "Magic in the World",
|
||||
"include_in_contents": "main_contents",
|
||||
"contents": {
|
||||
"id": "magical_world_subsections",
|
||||
"hyperlinks": true,
|
||||
"page_numbers": true,
|
||||
"separator": "."
|
||||
},
|
||||
"text": [
|
||||
|
||||
"One cannot master the arcane just by staying at home - there is a world full of magic out there to explore! Ancient ruins, relics and mysterious beings good and bad await discovery - if you know where to look. The greatest wizards are also the most curious, and gain much of their knowledge and power through exploration of the environment around them, and experimenting with what they find.",
|
||||
|
||||
"Whilst much of a wizard's time is inevitably spent alone, the sharing of knowledge is also vital to learning the arcane arts. Seek out fellow practicioners of magic, and learn as much as you can from them: communication is arguably the greatest power of all."
|
||||
],
|
||||
"sections": {
|
||||
"crystal_flowers": {
|
||||
"title": "Crystal Flowers",
|
||||
"include_in_contents": "magical_world_subsections",
|
||||
"triggers": [
|
||||
"ebwizardry:handbook/crystal_flowers"
|
||||
],
|
||||
"text": [
|
||||
|
||||
"During your travels, you will probably come across curious glowing flowers growing in the wild from time to time. These distinctive blooms are known as crystal flowers, and they are strangely effective at concentrating mana - to this day, nobody is quite sure why. We do know, however, that they can be harvested and crafted to extract the mana as crystals, making them a rather useful above-ground source of mana. The amount of mana obtainable this way is limited, though, by the small size of the flowers and due to them only growing in small patches.",
|
||||
|
||||
"#image crystal_flower"
|
||||
]
|
||||
},
|
||||
"wizard_towers": {
|
||||
"title": "Wizard Towers",
|
||||
"include_in_contents": "magical_world_subsections",
|
||||
"text": [
|
||||
|
||||
"Fear not - you are not the only practicioner of magic in this world. On your travels, you may well encounter a tall tower or two with a distinctive pointed roof. This is the residence of a fellow wizard. A life of solitude can make wizards a little grumpy at times, but they are usually friendly folk who are willing to share their knowledge with you - albeit for a price. Expect to pay precious metals and gems, and in return you will recieve many a great arcane wonder. If, however, it is a master spell you seek, you will need to speak to a specialist.",
|
||||
|
||||
"Unfortunately, there are a few wizards out there who do not welcome visitors. These wizards are typically outcasts, and are hostile to anyone crossing their path. Approach these individuals with caution, and be prepared to defend yourself against powerful magic. Defeat them however, and their knowledge is yours for the taking.",
|
||||
|
||||
"It should also be noted that no wizard will take kindly to being attacked or stolen from - and will readily take the law into their own hands."
|
||||
]
|
||||
},
|
||||
"obelisks": {
|
||||
"title": "Obelisks",
|
||||
"include_in_contents": "magical_world_subsections",
|
||||
"text": [
|
||||
|
||||
"Vestiges of ancient magic are scattered throughout the world, the most notable of which are carved stone structures which bear a great many symbols and runes. These ruins are all that remain of what appears to have been some kind of ancient civilisation. Whoever, or whatever, built them, they clearly had a considerable knowledge of magic, and used it to place protective enchantments over such locations that still persist to this day.",
|
||||
|
||||
"These stuctures fit broadly into two types. The first, and more common, of these are obelisks: tall spikes of carved stone, known as runestone, with an open structure at the bottom that usually contains minor arcane relics from the forgotten past. These structures are typically protected by an enchantment that summons hostile magical creatures should any human stray too close. Fend off these creatures and destroy their source, and the relics are yours."
|
||||
]
|
||||
},
|
||||
"shrines": {
|
||||
"title": "Shrines",
|
||||
"include_in_contents": "magical_world_subsections",
|
||||
"text": [
|
||||
|
||||
"The second, and rarer, kind of structure is known as a shrine. These structures consist of a circle of runestone pillars surrounding a central platform, upon which sits a chest filled with ancient artifacts. This chest is typically protected by an arcane lock enchantment, preventing anyone except the owner from opening it. The entire structure is also protected by a containment field, which prevents anything that strays too close from escaping.",
|
||||
|
||||
"Due to their great arcane power and significance, not to mention the riches within, these structures are particularly attractive to any aspiring wizard - but be cautious. There are numerous reports of wizards becoming trapped within containment fields and slowly being driven insane, perhaps by claustrophobia. A growing number, however, believe such occurences to be a deliberate part of the shrine's protective magic, and that the wizards trapped within are in fact being controlled to protect the structure. A chilling prospect, surely, for anyone who dares to venture near..."
|
||||
]
|
||||
},
|
||||
"creatures": {
|
||||
"title": "Magical Creatures",
|
||||
"include_in_contents": "magical_world_subsections",
|
||||
"text": [
|
||||
|
||||
"Besides wizards, a multitude of other creatures also use magic. Many of these creatures are arcane in origin, and most can be summoned using certain magical spells. One may encounter these creatures guarding an obelisk, or perhaps a powerful summoner. Lesser arcane beings are even found occasionally in the wilderness."
|
||||
]
|
||||
},
|
||||
"artefacts": {
|
||||
"title": "Artifacts",
|
||||
"include_in_contents": "magical_world_subsections",
|
||||
"text": [
|
||||
|
||||
"When exploring a shrine, you may be lucky enough to uncover some kind of ancient magical artifact - an object capable of granting its wearer unique and powerful buffs and special powers. Three types are known to have been found: rings, which grant bonuses and effects to spells, amulets, which improve defensive abilities, and trinkets, which give utility effects. These artifacts appear to function only when worn appropriately; simply having them on one's person is insufficient."
|
||||
]
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
"tiers": {
|
||||
"title": "Tiers",
|
||||
"include_in_contents": "main_contents",
|
||||
"text":[
|
||||
|
||||
"Wands and spells come in four tiers: #colour_novicenovice#colour_reset, #colour_apprenticeapprentice#colour_reset, #colour_advancedadvanced #colour_resetand #colour_mastermaster#colour_reset. Each tier is more powerful than the last.",
|
||||
|
||||
"#colour_noviceNovice #colour_resetwands are the ones that can be crafted. They hold up to #novice_max_charge mana, and can only cast novice spells. Novice spells are simple enough to be cast by anyone and they do not usually cost much mana. This does not necessarily mean that they are useless at higher tiers however; their low cost makes some novice spells useful even when you have access to master spells.",
|
||||
|
||||
"#colour_apprenticeApprentice #colour_resetwands are the next tier up from novice wands. They hold up to #apprentice_max_charge mana and can cast novice and apprentice spells. Apprentice spells are a little more impressive than novice spells but usually cost more.",
|
||||
|
||||
"#colour_advancedAdvanced #colour_resetwands are quite rare and are much more powerful. They can cast all spells except master spells, and hold #advanced_max_charge mana. Advanced spells can grant superhuman powers such as invisibility and wreak havoc on enemies.",
|
||||
|
||||
"#colour_masterMaster #colour_resetwands are the most powerful wands in existence. They hold up to #master_max_charge mana, and can cast any spell. Master spell books are very rare and the spells can cause complete devastation not only to enemies but even the world itself. Use with caution."
|
||||
]
|
||||
},
|
||||
|
||||
"elements": {
|
||||
"title": "Elements",
|
||||
"include_in_contents": "main_contents",
|
||||
"text": [
|
||||
|
||||
"Spells belong to different elements, which describe the nature of the spell and also have perks when used with certain wands.",
|
||||
|
||||
"#colour_fireFire#colour_reset \nPerhaps the most destructive element, fire concerns burning, lava, and explosions. A powerful pyromancer will unleash hell upon their enemies and probably set the world alight in the process. Most fire attack spells set fire to their target, causing ongoing damage over time. However, be wary that fire spells may have no effect on nether mobs.",
|
||||
|
||||
"#colour_iceIce#colour_reset \nThe element of ice is made of all that is cold. Frost spells often slow enemies down or freeze them completely, and are especially effective on creatures of fire. Frost magic can prove equally useful outside of combat, such as freezing water to cross a river.",
|
||||
|
||||
"#colour_lightningLightning#colour_reset \nThis element concerns lightning, storms, and the weather. A powerful storm mage is a force to be reckoned with, and some posess the ability to call down lightning at will. Lightning spells often damage multiple enemies at once and usually seek their target, making them an effective attack against any mob... but beware of creepers.",
|
||||
|
||||
"#colour_necromancyNecromancy#colour_reset \nNecromancy is the element of darkness, chaos and the undead. Necromancers are mysterious and often regarded as evil, though this is usually not the case. Necromancy spells are commonly used to summon creatures to fight for you or even bend the will of your enemies.",
|
||||
|
||||
"#colour_earthEarth#colour_reset \nThe element of earth is focused on the natural world: animals, plants, the wind, and such like. Earth magic is diverse and takes a wide variety of forms, from poisoning enemies to unleashing the fury of the weather. Earth spells are a mixture of attack, defense and utility.",
|
||||
|
||||
"#colour_sorcerySorcery#colour_reset \nSorcery is the element of force and change. Sorcerers manipulate light, gravity and even reality itself to fit their needs. Sorcery spells can be used, among other things, to grant their caster magical powers or move objects to their will.",
|
||||
|
||||
"#colour_healingHealing#colour_reset \nThe element of healing is concerned with defense and regeneration. Healers seek to protect themselves and their allies as much as possible and as such can be very difficult opponents. Though generally not used as an attack, the purifying light of some healing spells will do considerable damage to the undead. Healing spells are indispensable when in combat.",
|
||||
|
||||
"The boundaries between the different elements are not always distinct, and some spells bear traits of elements other than their own.",
|
||||
|
||||
"If you are lucky, you may happen upon an elemental wand. These wands will allow you to cast spells of the right element for more potency than usual."
|
||||
]
|
||||
},
|
||||
|
||||
"growing_crystals": {
|
||||
"title": "Growing Crystals",
|
||||
"include_in_contents": "main_contents",
|
||||
"text": [
|
||||
|
||||
"Magic crystals normally form naturally underground over thousands of years, by gradually drawing latent mana from the surrounding rock as they grow. Crystalline structures are very good at absorbing and holding mana, hence the mana is naturally drawn towards them. Given the right conditions, however, it is possible to greatly speed up this process. This has given rise to the advanced technique of crystal growing, which requires both great patience and attention to detail.",
|
||||
|
||||
"In order to encourage crystal growth, one must begin with a crystal seed. The most effective seeds have proven to be crystal shards, obtained by simply shattering a magic crystal into pieces. A crystal seed must then be planted into some kind of substrate, usually rock, although other materials are also viable. Left alone, this would produce a sizeable crystal in hundreds of years, depending on the conditions.",
|
||||
|
||||
"To reduce this length of time, several things can be done. Immersing the seed and substrate in mineral-rich water helps with crystal growth, but not with concentrating mana. To concentrate mana more quickly, two techniques can be employed: firstly, surrounding the growth site with lesser mana-gathering objects allows the crystal to draw from them, meaning the surrounding environment is constantly replenished with mana. Too many, however, and they begin to draw mana away from the growing crystal. Secondly, mana can be drawn through some materials better than others: air is very poor at this, whereas dense materials tend to be much better. Some wizards have taken to experimentation with exotic, magical, or even otherworldy materials to try and improve crystal growth. Some even claim to have altered the type of crystal that is formed in this way.",
|
||||
|
||||
"If conditions are exactly right, it is even possible for crystals to grow far beyond their natural size. Such crystals are known as grand magic crystals, and besides containing a great quantity of mana, they have a few specific uses."
|
||||
]
|
||||
},
|
||||
|
||||
"miscellaneous": {
|
||||
"title": "Miscellaneous",
|
||||
"include_in_contents": "main_contents",
|
||||
"contents": {
|
||||
"id": "miscellaneous_subsections",
|
||||
"hyperlinks": true,
|
||||
"page_numbers": true,
|
||||
"separator": "."
|
||||
},
|
||||
"sections": {
|
||||
"mana_flasks": {
|
||||
"title": "Mana Flasks",
|
||||
"include_in_contents": "miscellaneous_subsections",
|
||||
"text": [
|
||||
|
||||
"Arkendur's Arcane Supplies Co. - magical items for your every need!",
|
||||
|
||||
"Need to recharge your wand on the go? No problem! Mana can now be bottled.** Simply craft a mana flask with a glass bottle and eight magic crystals and take it with you. When you need to use it, simply craft it with your wand and it will restore some of the charge.*",
|
||||
|
||||
"NEW! Introducing the brand-new large*** and small mana flasks - now you can choose a size of mana flask to meet your needs!",
|
||||
|
||||
"* This process will consume the bottle.",
|
||||
"** Some mana is lost during bottling.",
|
||||
"*** Large mana flask requires grand magic crystals."
|
||||
]
|
||||
},
|
||||
"throwable_items": {
|
||||
"title": "Throwable Items",
|
||||
"include_in_contents": "miscellaneous_subsections",
|
||||
"text": [
|
||||
|
||||
"Various spells conjure physical items, some of which can also be crafted directly. Firebombs, poison bombs, spark bombs and smoke bombs can all be crafted."
|
||||
]
|
||||
},
|
||||
"automated_casting": {
|
||||
"title": "Automated Casting",
|
||||
"include_in_contents": "miscellaneous_subsections",
|
||||
"text": [
|
||||
|
||||
"Recent experimentation has revealed that it is possible to automate spell casting, to a degree, using nothing more than a simple dispenser. Nobody is quite sure why, but it would appear that the strange properties of redstone even extend to triggering the activation of spell scrolls. Placing a few into a dispenser and powering it ought to do the trick..."
|
||||
]
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
"crafting_recipes": {
|
||||
"title": "Crafting Recipes",
|
||||
"include_in_contents": "main_contents",
|
||||
"text": [
|
||||
|
||||
"#recipe arcane_workbench",
|
||||
"#recipe magic_wand",
|
||||
"#recipe magic_missile_spell_book",
|
||||
"#recipe wizard_handbook",
|
||||
"#recipe crystal_flower_to_crystals",
|
||||
"#recipe mana_flask",
|
||||
"#recipe transportation_stone",
|
||||
"#recipe magic_silk",
|
||||
"#recipe wizard_hat",
|
||||
"#recipe wizard_robe",
|
||||
"#recipe wizard_leggings",
|
||||
"#recipe wizard_boots",
|
||||
"#recipe blank_scroll",
|
||||
"#recipe firebomb",
|
||||
"#recipe poison_bomb",
|
||||
"#recipe smoke_bomb"
|
||||
]
|
||||
},
|
||||
|
||||
"credits": {
|
||||
"title": "Credits",
|
||||
"include_in_contents": "main_contents",
|
||||
"text": [
|
||||
|
||||
"Electroblob's Wizardry \nVersion #version \nFor Minecraft #mcversion",
|
||||
|
||||
"For more information, check out the @https://github.com/Electroblob77/Wizardry/wiki wiki@.",
|
||||
|
||||
"Designed, coded and textured by Electroblob",
|
||||
|
||||
"Thanks to Minecraft Forge and MCP, without which this mod would not have been possible.",
|
||||
|
||||
"Thanks also to the Minecraft modding community, which always has an answer to my modding problems!",
|
||||
|
||||
"In addition, I'd like to thank the following individuals for their contributions to the mod:",
|
||||
|
||||
"Code:",
|
||||
|
||||
"- Corail31 \n- 12foo \n- Shadows-of-Fire",
|
||||
|
||||
"Translations:",
|
||||
|
||||
"- Russian: VilagVil \n- Spanish and Mexican Spanish: MadWrist \n- Chinese: ZHENGLOC",
|
||||
|
||||
"Lightning ray sound effect from OhhWowProductions"
|
||||
]
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 15 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 84 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 26 KiB |
Reference in New Issue
Block a user