From c5aebc6bdb9e25e7a5e5f1c19acbbac4775fa11b Mon Sep 17 00:00:00 2001 From: TheBigEye <63316583+TheBigEye@users.noreply.github.com> Date: Mon, 10 Aug 2020 10:09:14 -0300 Subject: [PATCH] Add files via upload --- src/minicraft/screen/BookData.java | 28 + src/minicraft/screen/BookDisplay.java | 95 +++ src/minicraft/screen/ContainerDisplay.java | 114 ++++ src/minicraft/screen/CraftingDisplay.java | 109 ++++ src/minicraft/screen/Display.java | 105 ++++ src/minicraft/screen/EndGameDisplay.java | 111 ++++ src/minicraft/screen/InfoDisplay.java | 50 ++ src/minicraft/screen/InventoryMenu.java | 66 +++ src/minicraft/screen/ItemListMenu.java | 27 + src/minicraft/screen/KeyInputDisplay.java | 119 ++++ .../screen/LevelTransitionDisplay.java | 37 ++ src/minicraft/screen/LoadingDisplay.java | 79 +++ src/minicraft/screen/Menu.java | 546 ++++++++++++++++++ src/minicraft/screen/MultiplayerDisplay.java | 349 +++++++++++ src/minicraft/screen/OptionsDisplay.java | 33 ++ src/minicraft/screen/PauseDisplay.java | 106 ++++ src/minicraft/screen/PlayerDeathDisplay.java | 50 ++ src/minicraft/screen/PlayerInvDisplay.java | 30 + src/minicraft/screen/RecipeMenu.java | 29 + src/minicraft/screen/RelPos.java | 66 +++ src/minicraft/screen/TempDisplay.java | 49 ++ src/minicraft/screen/TitleDisplay.java | 284 +++++++++ src/minicraft/screen/WorldEditDisplay.java | 120 ++++ src/minicraft/screen/WorldGenDisplay.java | 114 ++++ src/minicraft/screen/WorldSelectDisplay.java | 202 +++++++ src/minicraft/screen/entry/ArrayEntry.java | 154 +++++ src/minicraft/screen/entry/BlankEntry.java | 26 + src/minicraft/screen/entry/BooleanEntry.java | 20 + .../screen/entry/ChangeListener.java | 6 + src/minicraft/screen/entry/InputEntry.java | 61 ++ src/minicraft/screen/entry/ItemEntry.java | 38 ++ src/minicraft/screen/entry/ItemListing.java | 21 + src/minicraft/screen/entry/KeyInputEntry.java | 47 ++ src/minicraft/screen/entry/LinkEntry.java | 66 +++ src/minicraft/screen/entry/ListEntry.java | 83 +++ src/minicraft/screen/entry/RangeEntry.java | 31 + src/minicraft/screen/entry/RecipeEntry.java | 41 ++ src/minicraft/screen/entry/SelectEntry.java | 47 ++ src/minicraft/screen/entry/StringEntry.java | 45 ++ 39 files changed, 3604 insertions(+) create mode 100644 src/minicraft/screen/BookData.java create mode 100644 src/minicraft/screen/BookDisplay.java create mode 100644 src/minicraft/screen/ContainerDisplay.java create mode 100644 src/minicraft/screen/CraftingDisplay.java create mode 100644 src/minicraft/screen/Display.java create mode 100644 src/minicraft/screen/EndGameDisplay.java create mode 100644 src/minicraft/screen/InfoDisplay.java create mode 100644 src/minicraft/screen/InventoryMenu.java create mode 100644 src/minicraft/screen/ItemListMenu.java create mode 100644 src/minicraft/screen/KeyInputDisplay.java create mode 100644 src/minicraft/screen/LevelTransitionDisplay.java create mode 100644 src/minicraft/screen/LoadingDisplay.java create mode 100644 src/minicraft/screen/Menu.java create mode 100644 src/minicraft/screen/MultiplayerDisplay.java create mode 100644 src/minicraft/screen/OptionsDisplay.java create mode 100644 src/minicraft/screen/PauseDisplay.java create mode 100644 src/minicraft/screen/PlayerDeathDisplay.java create mode 100644 src/minicraft/screen/PlayerInvDisplay.java create mode 100644 src/minicraft/screen/RecipeMenu.java create mode 100644 src/minicraft/screen/RelPos.java create mode 100644 src/minicraft/screen/TempDisplay.java create mode 100644 src/minicraft/screen/TitleDisplay.java create mode 100644 src/minicraft/screen/WorldEditDisplay.java create mode 100644 src/minicraft/screen/WorldGenDisplay.java create mode 100644 src/minicraft/screen/WorldSelectDisplay.java create mode 100644 src/minicraft/screen/entry/ArrayEntry.java create mode 100644 src/minicraft/screen/entry/BlankEntry.java create mode 100644 src/minicraft/screen/entry/BooleanEntry.java create mode 100644 src/minicraft/screen/entry/ChangeListener.java create mode 100644 src/minicraft/screen/entry/InputEntry.java create mode 100644 src/minicraft/screen/entry/ItemEntry.java create mode 100644 src/minicraft/screen/entry/ItemListing.java create mode 100644 src/minicraft/screen/entry/KeyInputEntry.java create mode 100644 src/minicraft/screen/entry/LinkEntry.java create mode 100644 src/minicraft/screen/entry/ListEntry.java create mode 100644 src/minicraft/screen/entry/RangeEntry.java create mode 100644 src/minicraft/screen/entry/RecipeEntry.java create mode 100644 src/minicraft/screen/entry/SelectEntry.java create mode 100644 src/minicraft/screen/entry/StringEntry.java diff --git a/src/minicraft/screen/BookData.java b/src/minicraft/screen/BookData.java new file mode 100644 index 0000000..2a6411d --- /dev/null +++ b/src/minicraft/screen/BookData.java @@ -0,0 +1,28 @@ +package minicraft.screen; + +import java.io.IOException; + +import minicraft.saveload.Load; + +public class BookData { + + public static final String about = "Modded by David.b and +Dillyg10+ until 1.8, then taken over by Chris J until 2.0.4, and then by afyber. Our goal is to expand Minicraft to be more fun and continuous.\nMinicraft was originally made by Markus Perrson for ludum dare 22 competition."; + + public static final String instructions = "With the default controls...\n\nMove your character with arrow keys or WSAD. Press C to attack and X to open the inventory, and to use items. Pickup furniture and torches with V. Select an item in the inventory to equip it.\n\nThe Goal: Defeat Cthulhu!"; + + public static final String antVenomBook = loadBook("antidous"); + public static final String storylineGuide = loadBook("story_guide"); + + private static final String loadBook(String bookTitle) { + String book; + try { + book = String.join("\n", Load.loadFile("/resources/"+bookTitle+".txt")); + book = book.replaceAll("\\\\0", "\0"); + } catch (IOException ex) { + ex.printStackTrace(); + book = ""; + } + + return book; + } +} diff --git a/src/minicraft/screen/BookDisplay.java b/src/minicraft/screen/BookDisplay.java new file mode 100644 index 0000000..b682b9e --- /dev/null +++ b/src/minicraft/screen/BookDisplay.java @@ -0,0 +1,95 @@ +package minicraft.screen; + +import java.util.ArrayList; +import java.util.Arrays; + +import minicraft.core.Game; +import minicraft.core.io.InputHandler; +import minicraft.core.io.Localization; +import minicraft.gfx.Color; +import minicraft.gfx.Font; +import minicraft.gfx.Point; +import minicraft.gfx.Screen; +import minicraft.gfx.SpriteSheet; +import minicraft.screen.entry.StringEntry; + +public class BookDisplay extends Display { + + // null characters "\0" denote page breaks. + private static final String defaultBook = " \n \0"+"There is nothing of use here."+"\0 \0"+"Still nothing... :P"; + + private static final int spacing = 3; + private static final int minX = 15, maxX = 15+8*32, minY = 8*5, maxY = 8*5+8*16; + + private String[][] lines; + private int page; + + private final boolean hasTitle; + private final boolean showPageCount; + private final int pageOffset; + + public BookDisplay(String book) { this(book, false); } + public BookDisplay(String book, boolean hasTitle) {// this(book, hasTitle, !hasTitle); } + //public BookDisplay(String book, boolean hasTitle, boolean hideCountIfOnePage) { + page = 0; + if(book == null) { + book = defaultBook; + hasTitle = false; + } + book = Localization.getLocalized(book); + this.hasTitle = hasTitle; + + ArrayList pages = new ArrayList<>(); + String[] splitContents = book.split("\0"); + for(String content: splitContents) { + String[] remainder = {content}; + while(remainder[remainder.length-1].length() > 0) { + remainder = Font.getLines(remainder[remainder.length-1], maxX-minX, maxY-minY, spacing, true); + pages.add(Arrays.copyOf(remainder, remainder.length-1)); // removes the last element of remainder, which is the leftover. + } + } + + lines = pages.toArray(new String[pages.size()][]); + + showPageCount = hasTitle || lines.length != 1; + pageOffset = showPageCount ? 1 : 0; + + Menu.Builder builder = new Menu.Builder(true, spacing, RelPos.CENTER); + + Menu pageCount = builder // the small rect for the title + .setPositioning(new Point(Screen.w/2, 0), RelPos.BOTTOM) + .setEntries(StringEntry.useLines(Color.BLACK, "Page", hasTitle?"Title":"1/"+lines.length)) + .setSelection(1) + .createMenu(); + + builder + .setPositioning(new Point(Screen.w/2, pageCount.getBounds().getBottom() + spacing), RelPos.BOTTOM) + .setSize(maxX-minX + SpriteSheet.boxWidth*2, maxY-minY + SpriteSheet.boxWidth*2) + .setShouldRender(false); + + menus = new Menu[lines.length+pageOffset]; + if(showPageCount) menus[0] = pageCount; + for(int i = 0; i < lines.length; i++) { + menus[i+pageOffset] = builder.setEntries(StringEntry.useLines(Color.WHITE, lines[i])).createMenu(); + } + + menus[page+pageOffset].shouldRender = true; + } + + private void turnPage(int dir) { + if(page+dir >= 0 && page+dir < lines.length) { + menus[page+pageOffset].shouldRender = false; + page += dir; + if(showPageCount) menus[0].updateSelectedEntry(new StringEntry(page==0 && hasTitle?"Title":(page+1)+"/"+lines.length, Color.BLACK)); + menus[page+pageOffset].shouldRender = true; + } + } + + @Override + public void tick(InputHandler input) { + if (input.getKey("menu").clicked || input.getKey("exit").clicked) + Game.exitMenu(); // this is what closes the book; TODO if books were editable, I would probably remake the book here with the edited pages. + if (input.getKey("cursor-left").clicked) turnPage(-1); // this is what turns the page back + if (input.getKey("cursor-right").clicked) turnPage(1); // this is what turns the page forward + } +} diff --git a/src/minicraft/screen/ContainerDisplay.java b/src/minicraft/screen/ContainerDisplay.java new file mode 100644 index 0000000..c9c805b --- /dev/null +++ b/src/minicraft/screen/ContainerDisplay.java @@ -0,0 +1,114 @@ +package minicraft.screen; + +import minicraft.core.Game; +import minicraft.core.io.InputHandler; +import minicraft.entity.ItemHolder; +import minicraft.entity.furniture.Chest; +import minicraft.entity.mob.Player; +import minicraft.gfx.Screen; +import minicraft.item.Inventory; +import minicraft.item.Item; +import minicraft.item.StackableItem; + +public class ContainerDisplay extends Display { + + private static final int padding = 10; + + private Player player; + private Chest chest; + + public ContainerDisplay(Player player, Chest chest) { + super(new InventoryMenu(chest, chest.getInventory(), chest.name), new InventoryMenu(player, player.getInventory(), "Inventory")); + //pInv = player.getInventory(); + //cInv = chest.getInventory(); + this.player = player; + this.chest = chest; + + menus[1].translate(menus[0].getBounds().getWidth() + padding, 0); + + if(menus[0].getNumOptions() == 0) onSelectionChange(0, 1); + } + + @Override + protected void onSelectionChange(int oldSel, int newSel) { + super.onSelectionChange(oldSel, newSel); + if(oldSel == newSel) return; // this also serves as a protection against access to menus[0] when such may not exist. + int shift = 0; + if(newSel == 0) shift = padding - menus[0].getBounds().getLeft(); + if(newSel == 1) shift = (Screen.w - padding) - menus[1].getBounds().getRight(); + for(Menu m: menus) + m.translate(shift, 0); + } + + private int getOtherIdx() { return (selection+1) % 2; } + + @Override + public void tick(InputHandler input) { + super.tick(input); + + if(input.getKey("menu").clicked || chest.isRemoved()) { + Game.setMenu(null); + return; + } + + Menu curMenu = menus[selection]; + int otherIdx = getOtherIdx(); + + if(curMenu.getNumOptions() > 0 && (input.getKey("attack").clicked || input.getKey("drop-one").clicked)) { + // switch inventories + Inventory from, to; + if(selection == 0) { + from = chest.getInventory(); + to = player.getInventory(); + } else { + from = player.getInventory(); + to = chest.getInventory(); + } + + int toSel = menus[otherIdx].getSelection(); + int fromSel = curMenu.getSelection(); + + if(Game.isValidClient() && from == chest.getInventory()) { + // just send, take no actual action + Game.client.removeFromChest(chest, fromSel, toSel, input.getKey("attack").clicked); + return; + } + + Item fromItem = from.get(fromSel); + + boolean transferAll = input.getKey("attack").clicked || !(fromItem instanceof StackableItem) || ((StackableItem)fromItem).count == 1; + + Item toItem = fromItem.clone(); + + if(!transferAll) { + ((StackableItem)fromItem).count--; // this is known to be valid. + ((StackableItem)toItem).count = 1; + // items are setup for sending. + } + else { // transfer whole item/stack. + if(! (Game.isMode("creative") && from == player.getInventory()) ) + from.remove(fromSel); // remove it + } + + if(!Game.isValidClient()) { + to.add(toSel, toItem); + update(); + } else if(to == chest.getInventory()) { + // is online client, and from == player + Game.client.addToChest(chest, toSel, toItem); + } + } + } + + public void onInvUpdate(ItemHolder holder) { + if(holder == player || holder == chest) + update(); + } + + private void update() { + menus[0] = new InventoryMenu((InventoryMenu) menus[0]); + menus[1] = new InventoryMenu((InventoryMenu) menus[1]); + menus[1].translate(menus[0].getBounds().getWidth() + padding, 0); + onSelectionChange(0, selection); + } +} diff --git a/src/minicraft/screen/CraftingDisplay.java b/src/minicraft/screen/CraftingDisplay.java new file mode 100644 index 0000000..ffe9522 --- /dev/null +++ b/src/minicraft/screen/CraftingDisplay.java @@ -0,0 +1,109 @@ +package minicraft.screen; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; + +import minicraft.core.Game; +import minicraft.core.io.InputHandler; +import minicraft.entity.mob.Player; +import minicraft.gfx.Point; +import minicraft.gfx.SpriteSheet; +import minicraft.item.Item; +import minicraft.item.Items; +import minicraft.item.Recipe; +import minicraft.screen.entry.ItemListing; + +public class CraftingDisplay extends Display { + + private Player player; + private Recipe[] recipes; + + private RecipeMenu recipeMenu; + private Menu.Builder itemCountMenu, costsMenu; + + private boolean isPersonalCrafter; + + public CraftingDisplay(List recipes, String title, Player player) { this(recipes, title, player, false); } + public CraftingDisplay(List recipes, String title, Player player, boolean isPersonal) { + for(Recipe recipe: recipes) + recipe.checkCanCraft(player); + + this.isPersonalCrafter = isPersonal; + + if(!isPersonal) + recipeMenu = new RecipeMenu(recipes, title, player); + else + recipeMenu = new RecipeMenu(recipes, title, player); + + this.player = player; + this.recipes = recipes.toArray(new Recipe[recipes.size()]); + + itemCountMenu = new Menu.Builder(true, 0, RelPos.LEFT) + .setTitle("Have:") + .setTitlePos(RelPos.TOP_LEFT) + .setPositioning(new Point(recipeMenu.getBounds().getRight()+SpriteSheet.boxWidth, recipeMenu.getBounds().getTop()), RelPos.BOTTOM_RIGHT); + + costsMenu = new Menu.Builder(true, 0, RelPos.LEFT) + .setTitle("Cost:") + .setTitlePos(RelPos.TOP_LEFT) + .setPositioning(new Point(itemCountMenu.createMenu().getBounds().getLeft(), recipeMenu.getBounds().getBottom()), RelPos.TOP_RIGHT); + + menus = new Menu[] {recipeMenu, itemCountMenu.createMenu(), costsMenu.createMenu()}; + + refreshData(); + } + + private void refreshData() { + Menu prev = menus[2]; + menus[2] = costsMenu + .setEntries(getCurItemCosts()) + .createMenu(); + menus[2].setColors(prev); + + menus[1] = itemCountMenu + .setEntries(new ItemListing(recipes[recipeMenu.getSelection()].getProduct(), String.valueOf(getCurItemCount()))) + .createMenu(); + menus[1].setColors(prev); + } + + private int getCurItemCount() { + return player.getInventory().count(recipes[recipeMenu.getSelection()].getProduct()); + } + + private ItemListing[] getCurItemCosts() { + ArrayList costList = new ArrayList<>(); + HashMap costMap = recipes[recipeMenu.getSelection()].getCosts(); + for(String itemName: costMap.keySet()) { + Item cost = Items.get(itemName); + costList.add(new ItemListing(cost, costMap.get(itemName)+"/"+player.getInventory().count(cost))); + } + + return costList.toArray(new ItemListing[costList.size()]); + } + + @Override + public void tick(InputHandler input) { + if(input.getKey("menu").clicked || (isPersonalCrafter && input.getKey("craft").clicked)) { + Game.exitMenu(); + return; + } + + int prevSel = recipeMenu.getSelection(); + super.tick(input); + if(prevSel != recipeMenu.getSelection()) + refreshData(); + + if((input.getKey("select").clicked || input.getKey("attack").clicked) && recipeMenu.getSelection() >= 0) { + // check the selected recipe + Recipe r = recipes[recipeMenu.getSelection()]; + if(r.getCanCraft()) { + r.craft(player); + + refreshData(); + for(Recipe recipe: recipes) + recipe.checkCanCraft(player); + } + } + } +} diff --git a/src/minicraft/screen/Display.java b/src/minicraft/screen/Display.java new file mode 100644 index 0000000..b2e695c --- /dev/null +++ b/src/minicraft/screen/Display.java @@ -0,0 +1,105 @@ +package minicraft.screen; + +import minicraft.core.Game; +import minicraft.core.io.InputHandler; +import minicraft.core.io.Sound; +import minicraft.gfx.Screen; +import minicraft.screen.entry.ArrayEntry; + +import org.jetbrains.annotations.Nullable; + +public class Display { + + private Display parent = null; + + protected Menu[] menus; + int selection; + + private final boolean canExit, clearScreen; + + public Display() { this(new Menu[0]); } + public Display(Menu... menus) { this(false, true, menus); } + public Display(boolean clearScreen) { this(clearScreen, true, new Menu[0]); } + public Display(boolean clearScreen, Menu... menus) { this(clearScreen, true, menus); } + public Display(boolean clearScreen, boolean canExit) { this(clearScreen, canExit, new Menu[0]); } + public Display(boolean clearScreen, boolean canExit, Menu... menus) { + this.menus = menus; + this.canExit = canExit; + this.clearScreen = clearScreen; + selection = 0; + } + + private boolean setParent = false; + // called during setMenu() + public void init(@Nullable Display parent) { + if(!setParent) { + setParent = true; + this.parent = parent; + } + } + + public void onExit() {} + + public Display getParent() { return parent; } + + public void tick(InputHandler input) { + + if(canExit && input.getKey("exit").clicked) { + Game.exitMenu(); + return; + } + + if(menus.length == 0) return; + + boolean changedSelection = false; + + if(menus.length > 1 && menus[selection].isSelectable()) { // if menu set is unselectable, it must have been intentional, so prevent the user from setting it back. + int prevSel = selection; + + String shift = menus[selection].getCurEntry() instanceof ArrayEntry ? "shift-" : ""; + if (input.getKey(shift+"left").clicked) selection--; + if (input.getKey(shift+"right").clicked) selection++; + + if(prevSel != selection) { + Sound.select.play(); + + int delta = selection - prevSel; + selection = prevSel; + do { + selection += delta; + if (selection < 0) selection = menus.length - 1; + selection = selection % menus.length; + } while(!menus[selection].isSelectable() && selection != prevSel); + + changedSelection = prevSel != selection; + } + + if(changedSelection) + onSelectionChange(prevSel, selection); + } + + if(!changedSelection) + menus[selection].tick(input); + } + + protected void onSelectionChange(int oldSel, int newSel) { + selection = newSel; + } + + /// sub-classes can do extra rendering here; this renders each menu that should be rendered, in the order of the array, such that the currently selected menu is rendered last, so it appears on top (if they even overlap in the first place). + public void render(Screen screen) { + if(clearScreen) + screen.clear(0); + + if(menus.length == 0) + return; + + int idx = selection; + do { + idx++; + idx = idx % menus.length; + if(menus[idx].shouldRender()) + menus[idx].render(screen); + } while(idx != selection); + } +} diff --git a/src/minicraft/screen/EndGameDisplay.java b/src/minicraft/screen/EndGameDisplay.java new file mode 100644 index 0000000..c0e381a --- /dev/null +++ b/src/minicraft/screen/EndGameDisplay.java @@ -0,0 +1,111 @@ +package minicraft.screen; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Random; + +import minicraft.core.Game; +import minicraft.core.Updater; +import minicraft.core.io.InputHandler; +import minicraft.core.io.Settings; +import minicraft.entity.mob.Player; +import minicraft.gfx.Color; +import minicraft.gfx.Screen; +import minicraft.item.Items; +import minicraft.saveload.Save; +import minicraft.screen.entry.ListEntry; +import minicraft.screen.entry.SelectEntry; +import minicraft.screen.entry.StringEntry; + +public class EndGameDisplay extends Display { + private static final Random random = new Random(); + + private static final String[] scoredItems = { + "Cloth", "Slime", "Bone", "Arrow", "Gunpowder", "Antidious" + }; + private static final int maxLen; + static { + int maxLength = 0; + for(String s: scoredItems) + maxLength = Math.max(maxLength, s.length()); + maxLen = maxLength; + } + + private int inputDelay; // variable to delay the input of the player, so they won't skip the won menu by accident. + private int displayTimer; + private int finalscore; + + public EndGameDisplay(Player player) { + super(false, false); + + displayTimer = Updater.normSpeed; // wait 3 seconds before rendering the menu. + inputDelay = Updater.normSpeed/2; // wait a half-second after rendering before allowing user input. + + + ArrayList entries = new ArrayList<>(); + + // calculate the score + entries.add(new StringEntry("Player Score: " + Game.player.getScore(), Color.WHITE)); + entries.add(new StringEntry("", Color.YELLOW)); + + finalscore = Game.player.getScore(); + for(String item: scoredItems) + addBonus(item); + + entries.add(new StringEntry("Final Score: " + finalscore)); + + // add any unlocks + entries.addAll(Arrays.asList(getAndWriteUnlocks())); + + entries.add(new SelectEntry("Exit to Menu", () -> Game.setMenu(new TitleDisplay()))); + + menus = new Menu[] { + new Menu.Builder(true, 0, RelPos.LEFT, entries).createMenu() + }; + } + + private StringEntry addBonus(String item) { + int count = Game.player.getInventory().count(Items.get(item)); + int score = count * (random.nextInt(2) + 1) * 10; + finalscore += score; + StringBuilder buffer = new StringBuilder(); + while(item.length()+buffer.length() < maxLen) buffer.append(" "); + return new StringEntry(count+" "+item+"s: "+buffer+"+"+score, Color.YELLOW); + } + + @Override + public void tick(InputHandler input) { + if (displayTimer > 0) displayTimer--; + else if (inputDelay > 0) inputDelay--; + else super.tick(input); + } + + @Override + public void render(Screen screen) { + if(displayTimer <= 0) + super.render(screen); + } + + private StringEntry[] getAndWriteUnlocks() { + int scoreTime = (int)Settings.get("scoretime"); + ArrayList unlocks = new ArrayList<>(); + + if(scoreTime == 20 && !Settings.getEntry("scoretime").valueIs(10) && finalscore > 1000) { + unlocks.add(10); + Settings.getEntry("scoretime").setValueVisibility(10, true); + } + + if(scoreTime == 60 && !Settings.getEntry("scoretime").valueIs(120) && finalscore > 100000) { + unlocks.add(120); + Settings.getEntry("scoretime").setValueVisibility(120, true); + } + + StringEntry[] entries = new StringEntry[unlocks.size()]; + for(int i = 0; i < entries.length; i++) + entries[i] = new StringEntry("Unlocked! " + unlocks.get(i) + " Score Time"); + + new Save(); // writes unlocks, and preferences + + return entries; + } +} diff --git a/src/minicraft/screen/InfoDisplay.java b/src/minicraft/screen/InfoDisplay.java new file mode 100644 index 0000000..2b18831 --- /dev/null +++ b/src/minicraft/screen/InfoDisplay.java @@ -0,0 +1,50 @@ +package minicraft.screen; + +import minicraft.core.Game; +import minicraft.core.Updater; +import minicraft.core.io.InputHandler; +import minicraft.gfx.Point; +import minicraft.gfx.SpriteSheet; +import minicraft.screen.entry.StringEntry; + +public class InfoDisplay extends Display { + + public InfoDisplay() { + //noinspection SuspiciousNameCombination + super(new Menu.Builder(true, 4, RelPos.LEFT, StringEntry.useLines( + "----------------------------", + "Time Played: " + getTimeString(), + "Current Score: " + Game.player.getScore(), + "----------------------------", + Game.input.getMapping("select")+"/"+Game.input.getMapping("exit")+":Exit" + )) + .setTitle("Player Stats") + .setTitlePos(RelPos.TOP_LEFT) + .setPositioning(new Point(SpriteSheet.boxWidth, SpriteSheet.boxWidth), RelPos.BOTTOM_RIGHT) + .createMenu() + ); + } + + @Override + public void tick(InputHandler input) { + if (input.getKey("select").clicked || input.getKey("exit").clicked) + Game.exitMenu(); + } + + public static String getTimeString() { + int seconds = Updater.gameTime / Updater.normSpeed; + int minutes = seconds / 60; + int hours = minutes / 60; + minutes %= 60; + seconds %= 60; + + String timeString; + if (hours > 0) { + timeString = hours + "h" + (minutes < 10 ? "0" : "") + minutes + "m"; + } else { + timeString = minutes + "m " + (seconds < 10 ? "0" : "") + seconds + "s"; + } + + return timeString; + } +} diff --git a/src/minicraft/screen/InventoryMenu.java b/src/minicraft/screen/InventoryMenu.java new file mode 100644 index 0000000..6ac621b --- /dev/null +++ b/src/minicraft/screen/InventoryMenu.java @@ -0,0 +1,66 @@ +package minicraft.screen; + +import minicraft.core.Game; +import minicraft.core.io.InputHandler; +import minicraft.entity.Entity; +import minicraft.entity.mob.Player; +import minicraft.item.Inventory; +import minicraft.item.Item; +import minicraft.item.StackableItem; +import minicraft.screen.entry.ItemEntry; + +class InventoryMenu extends ItemListMenu { + + private Inventory inv; + private Entity holder; + + InventoryMenu(Entity holder, Inventory inv, String title) { + super(ItemEntry.useItems(inv.getItems()), title); + this.inv = inv; + this.holder = holder; + } + + InventoryMenu(InventoryMenu model) { + super(ItemEntry.useItems(model.inv.getItems()), model.getTitle()); + this.inv = model.inv; + this.holder = model.holder; + setSelection(model.getSelection()); + } + + @Override + public void tick(InputHandler input) { + super.tick(input); + + boolean dropOne = input.getKey("drop-one").clicked && !(Game.getMenu() instanceof ContainerDisplay); + + if(getNumOptions() > 0 && (dropOne || input.getKey("drop-stack").clicked)) { + ItemEntry entry = ((ItemEntry)getCurEntry()); + if(entry == null) return; + Item invItem = entry.getItem(); + Item drop = invItem.clone(); + + if(dropOne && drop instanceof StackableItem && ((StackableItem)drop).count > 1) { + // just drop one from the stack + ((StackableItem)drop).count = 1; + ((StackableItem)invItem).count--; + } else { + // drop the whole item. + if(!Game.isMode("creative") || !(holder instanceof Player)) + removeSelectedEntry(); + } + + if(holder.getLevel() != null) { + if(Game.isValidClient()) + Game.client.dropItem(drop); + else + holder.getLevel().dropItem(holder.x, holder.y, drop); + } + } + } + + @Override + public void removeSelectedEntry() { + inv.remove(getSelection()); + super.removeSelectedEntry(); + } +} diff --git a/src/minicraft/screen/ItemListMenu.java b/src/minicraft/screen/ItemListMenu.java new file mode 100644 index 0000000..d958da9 --- /dev/null +++ b/src/minicraft/screen/ItemListMenu.java @@ -0,0 +1,27 @@ +package minicraft.screen; + +import minicraft.gfx.Point; +import minicraft.screen.entry.ItemEntry; + +class ItemListMenu extends Menu { + + static Builder getBuilder() { + return new Builder(true, 0, RelPos.LEFT) + .setPositioning(new Point(9, 9), RelPos.BOTTOM_RIGHT) + .setDisplayLength(9) + .setSelectable(true) + .setScrollPolicies(1, false); + } + + ItemListMenu(Builder b, ItemEntry[] entries, String title) { + super(b + .setEntries(entries) + .setTitle(title) + .createMenu() + ); + } + + ItemListMenu(ItemEntry[] entries, String title) { + this(getBuilder(), entries, title); + } +} diff --git a/src/minicraft/screen/KeyInputDisplay.java b/src/minicraft/screen/KeyInputDisplay.java new file mode 100644 index 0000000..d175c86 --- /dev/null +++ b/src/minicraft/screen/KeyInputDisplay.java @@ -0,0 +1,119 @@ +package minicraft.screen; + +import minicraft.core.Game; +import minicraft.core.io.InputHandler; +import minicraft.gfx.Color; +import minicraft.gfx.Font; +import minicraft.gfx.Point; +import minicraft.gfx.Screen; +import minicraft.screen.entry.KeyInputEntry; +import minicraft.screen.entry.StringEntry; + +public class KeyInputDisplay extends Display { + + private boolean listeningForBind, confirmReset; + + private static Menu.Builder builder; + + private static KeyInputEntry[] getEntries() { + String[] prefs = Game.input.getKeyPrefs(); + KeyInputEntry[] entries = new KeyInputEntry[prefs.length]; + + for (int i = 0; i < entries.length; i++) + entries[i] = new KeyInputEntry(prefs[i]); + + return entries; + } + + public KeyInputDisplay() { + super(true); + builder = new Menu.Builder(false, 0, RelPos.CENTER, getEntries()) + .setTitle("Controls") + .setPositioning(new Point(Screen.w/2, Screen.h - Font.textHeight()*5), RelPos.TOP); + + Menu.Builder popupBuilder = new Menu.Builder(true, 4, RelPos.CENTER) + .setShouldRender(false) + .setSelectable(false); + + menus = new Menu[] { + builder.createMenu(), + + popupBuilder + .setEntries(StringEntry.useLines(Color.YELLOW, "Press the desired", "key sequence")) + .createMenu(), + + popupBuilder + .setEntries(StringEntry.useLines(Color.RED, "Are you sure you want to reset all key bindings to the default keys?", "enter to confirm", "escape to cancel")) + .setTitle("Confirm Action") + .createMenu() + }; + + listeningForBind = false; + confirmReset = false; + } + + @Override + public void tick(InputHandler input) { + if(listeningForBind) { + if(input.keyToChange == null) { + // the key has just been set + listeningForBind = false; + menus[1].shouldRender = false; + menus[0].updateSelectedEntry(new KeyInputEntry(input.getChangedKey())); + selection = 0; + } + + return; + } + + if(confirmReset) { + if(input.getKey("exit").clicked) { + confirmReset = false; + menus[2].shouldRender = false; + selection = 0; + } + else if(input.getKey("select").clicked) { + confirmReset = false; + input.resetKeyBindings(); + menus[2].shouldRender = false; + menus[0] = builder.setEntries(getEntries()) + .setSelection(menus[0].getSelection(), menus[0].getDispSelection()) + .createMenu() + ; + selection = 0; + } + + return; + } + + super.tick(input); // ticks menu + + if(input.keyToChange != null) { + listeningForBind = true; + selection = 1; + menus[selection].shouldRender = true; + } else if(input.getKey("shift-d").clicked && !confirmReset) { + confirmReset = true; + selection = 2; + menus[selection].shouldRender = true; + } + } + + public void render(Screen screen) { + if(selection == 0) // not necessary to put in if statement now, but it's probably more efficient anyway + screen.clear(0); + + super.render(screen); + + if(!listeningForBind && !confirmReset) { + String[] lines = { + "Press C/Enter to change key binding", + "Press A to add key binding", + "Shift-D to reset all keys to default", + Game.input.getMapping("exit")+" to Return to menu" + }; + for(int i = 0; i < lines.length; i++) + Font.drawCentered(lines[i], screen, Screen.h-Font.textHeight()*(4-i), Color.WHITE); + } + } +} diff --git a/src/minicraft/screen/LevelTransitionDisplay.java b/src/minicraft/screen/LevelTransitionDisplay.java new file mode 100644 index 0000000..84e8cc6 --- /dev/null +++ b/src/minicraft/screen/LevelTransitionDisplay.java @@ -0,0 +1,37 @@ +package minicraft.screen; + +import minicraft.core.Game; +import minicraft.core.io.InputHandler; +import minicraft.core.World; +import minicraft.gfx.Screen; + +public class LevelTransitionDisplay extends Display { + + private static final int DURATION = 30; + + private int dir; // Direction that you are changing levels. (going up or down stairs) + private int time = 0; // Time it spends on this menu + + public LevelTransitionDisplay(int dir) { + super(false,false); + this.dir = dir; + } + + public void tick(InputHandler input) { + time++; // Ticks up 2 times per tick + if (time == DURATION/2) World.changeLevel(dir); // When time equals 30, it will change the level + if (time == DURATION) Game.setMenu(null); // When time equals 60, it will get out of this menu + } + + public void render(Screen screen) { + for (int x = 0; x < 200; x++) { // Loop however many times depending on the width (It's divided by 3 because the pixels are scaled up by 3) + for (int y = 0; y < 150; y++) { // Loop however many times depending on the height (It's divided by 3 because the pixels are scaled up by 3) + int dd = (y + x % 2 * 2 + x / 3) - time*2; // Used as part of the positioning. + if (dd < 0 && dd > -30) { + if (dir > 0) screen.render(x * 8, y * 8, 30 + 30 * 32, 0, 3); // If the direction is upwards then render the squares going up + else screen.render(x * 8, Screen.h - y * 8 - 8, 30 + 30 * 32, 0, 3); // If the direction is negative, then the squares will go down. + } + } + } + } +} diff --git a/src/minicraft/screen/LoadingDisplay.java b/src/minicraft/screen/LoadingDisplay.java new file mode 100644 index 0000000..fee4546 --- /dev/null +++ b/src/minicraft/screen/LoadingDisplay.java @@ -0,0 +1,79 @@ +package minicraft.screen; + +import javax.swing.Timer; + +import minicraft.core.Game; +import minicraft.core.World; +import minicraft.core.io.Localization; +import minicraft.gfx.Color; +import minicraft.gfx.Ellipsis; +import minicraft.gfx.Ellipsis.DotUpdater.TimeUpdater; +import minicraft.gfx.Ellipsis.SmoothEllipsis; +import minicraft.gfx.Font; +import minicraft.gfx.FontStyle; +import minicraft.gfx.Screen; +import minicraft.saveload.Save; + +public class LoadingDisplay extends Display { + + private static float percentage = 0; + private static String progressType = ""; + + private Timer t; + private String msg = ""; + private Ellipsis ellipsis = new SmoothEllipsis(new TimeUpdater()); + + public LoadingDisplay() { + super(true, false); + t = new Timer(500, e -> { + World.initWorld(); + Game.setMenu(null); + }); + t.setRepeats(false); + } + + @Override + public void init(Display parent) { + super.init(parent); + percentage = 0; + progressType = "World"; + if(WorldSelectDisplay.loadedWorld()) + msg = "Loading"; + else + msg = "Melting"; + t.start(); + + + } + + @Override + public void onExit() { + percentage = 0; + if(!WorldSelectDisplay.loadedWorld()) { + msg = "Planting"; + progressType = "World"; + new Save(WorldSelectDisplay.getWorldName()); + Game.notifications.clear(); + } + } + + public static void setPercentage(float percent) { + percentage = percent; + } + public static float getPercentage() { return percentage; } + public static void setMessage(String progressType) { LoadingDisplay.progressType = progressType; } + + public static void progress(float amt) { + percentage = Math.min(100, percentage+amt); + } + + @Override + public void render(Screen screen) { + super.render(screen); + int percent = Math.round(percentage); + Font.drawParagraph(screen, new FontStyle(Color.GREEN), 6, + Localization.getLocalized(msg)+ ellipsis.updateAndGet(), + percent+"%" + ); + } +} diff --git a/src/minicraft/screen/Menu.java b/src/minicraft/screen/Menu.java new file mode 100644 index 0000000..e1b069e --- /dev/null +++ b/src/minicraft/screen/Menu.java @@ -0,0 +1,546 @@ +package minicraft.screen; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; + +import minicraft.core.io.InputHandler; +import minicraft.core.io.Localization; +import minicraft.core.io.Sound; +import minicraft.gfx.*; +import minicraft.screen.entry.BlankEntry; +import minicraft.screen.entry.ListEntry; + +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +public class Menu { + + @NotNull + private ArrayList entries = new ArrayList<>(); + + private int spacing = 0; + private Rectangle bounds = null; + private Rectangle entryBounds = null; + private RelPos entryPos = RelPos.CENTER; // the x part of this is re-applied per entry, while the y part is calculated once using the cumulative height of all entries and spacing. + + private String title = ""; + private int titleColor; + private Point titleLoc = null; // standard point is anchor, with anchor.x + SpriteSheet.boxWidth + private boolean drawVertically = false; + + private boolean hasFrame; + + private boolean selectable = false; + boolean shouldRender = true; + + private int displayLength = 0; + private int padding = 0; + private boolean wrap = false; + + // menu selection vars + private int selection = 0; + private int dispSelection = 0; + private int offset = 0; + + + private Menu() {} + protected Menu(Menu m) { + entries.addAll(m.entries); + spacing = m.spacing; + bounds = m.bounds == null ? null : new Rectangle(m.bounds); + entryBounds = m.entryBounds == null ? null : new Rectangle(m.entryBounds); + entryPos = m.entryPos; + title = m.title; + titleColor = m.titleColor; + titleLoc = m.titleLoc; + drawVertically = m.drawVertically; + hasFrame = m.hasFrame; + selectable = m.selectable; + shouldRender = m.shouldRender; + displayLength = m.displayLength; + padding = m.padding; + wrap = m.wrap; + selection = m.selection; + dispSelection = m.dispSelection; + offset = m.offset; + } + + public void init() { + + if(entries.size() == 0) { + selection = 0; + dispSelection = 0; + offset = 0; + return; + } + + selection = Math.min(selection, entries.size()-1); + selection = Math.max(0, selection); + + if(!entries.get(selection).isSelectable()) { + int prevSel = selection; + do { + selection++; + if (selection < 0) selection = entries.size() - 1; + selection = selection % entries.size(); + } while (!entries.get(selection).isSelectable() && selection != prevSel); + } + + dispSelection = selection; + dispSelection = Math.min(dispSelection, displayLength-1); + dispSelection = Math.max(0, dispSelection); + + doScroll(); + } + + void setSelection(int idx) { + if(idx >= entries.size()) + idx = entries.size() - 1; + + if(idx < 0) idx = 0; + + this.selection = idx; + + doScroll(); + } + int getSelection() { return selection; } + int getDispSelection() { return dispSelection; } + + ListEntry[] getEntries() { return entries.toArray(new ListEntry[entries.size()]); } + @Nullable ListEntry getCurEntry() { return entries.size() == 0 ? null : entries.get(selection); } + int getNumOptions() { return entries.size(); } + + Rectangle getBounds() { return new Rectangle(bounds); } + String getTitle() { return title; } + + boolean isSelectable() { return selectable; } + boolean shouldRender() { return shouldRender; } + + /** @noinspection SameParameterValue*/ + void translate(int xoff, int yoff) { + bounds.translate(xoff, yoff); + entryBounds.translate(xoff, yoff); + titleLoc.translate(xoff, yoff); + } + + public void tick(InputHandler input) { + if(!selectable || entries.size() == 0) return; + + int prevSel = selection; + if(input.getKey("cursor-up").clicked) selection--; + if(input.getKey("cursor-down").clicked) selection++; + + int delta = selection - prevSel; + selection = prevSel; + if(delta == 0) { + entries.get(selection).tick(input); // only ticks the entry on a frame where the selection cursor has not moved. + return; + } else + Sound.select.play(); + + do { + selection += delta; + if (selection < 0) selection = entries.size() - 1; + selection = selection % entries.size(); + } while(!entries.get(selection).isSelectable() && selection != prevSel); + + // update offset and selection displayed + dispSelection += selection - prevSel; + + if(dispSelection < 0) dispSelection = 0; + if(dispSelection >= displayLength) dispSelection = displayLength - 1; + + doScroll(); + } + + private void doScroll() { + // check if dispSelection is past padding point, and if so, bring it back in + + dispSelection = selection - offset; + int offset = this.offset; + + // for scrolling up + while((dispSelection < padding || !wrap && offset + displayLength > entries.size()) && (wrap || offset > 0)) { + offset--; + dispSelection++; + } + + // for scrolling down + while((displayLength - dispSelection <= padding || !wrap && offset < 0) && (wrap || offset + displayLength < entries.size())) { + offset++; + dispSelection--; + } + + // only useful when wrap is true + if(offset < 0) offset += entries.size(); + if(offset > 0) offset = offset % entries.size(); + + this.offset = offset; + } + + public void render(Screen screen) { + renderFrame(screen); + + // render the title + if(title.length() > 0) { + if (drawVertically) { + for (int i = 0; i < title.length(); i++) { + if (hasFrame) screen.render(titleLoc.x, titleLoc.y + i * Font.textHeight(), 3 + 21 * 32, 0, 3); + Font.draw(title.substring(i, i + 1), screen, titleLoc.x, titleLoc.y + i * Font.textHeight(), titleColor); + } + } else { + for (int i = 0; i < title.length(); i++) { + if (hasFrame) screen.render(titleLoc.x + i * Font.textWidth(" "), titleLoc.y, 3 + 21 * 32, 0, 3); + Font.draw(title.substring(i, i + 1), screen, titleLoc.x + i * Font.textWidth(" "), titleLoc.y, titleColor); + } + } + } + + // render the options + int y = entryBounds.getTop(); + boolean special = wrap && entries.size() < displayLength; + if(special) { + int diff = displayLength - entries.size(); // we have to account for this many entry heights. + int extra = diff*(ListEntry.getHeight()+spacing) / 2; + y += extra; + } + for(int i = offset; i < (wrap?offset+displayLength:Math.min(offset+displayLength, entries.size())); i++) { + if(special && i-offset >= entries.size()) break; + + int idx = i % entries.size(); + ListEntry entry = entries.get(idx); + + if(!(entry instanceof BlankEntry)) { + Point pos = entryPos.positionRect(new Dimension(entry.getWidth(), ListEntry.getHeight()), new Rectangle(entryBounds.getLeft(), y, entryBounds.getWidth(), ListEntry.getHeight(), Rectangle.CORNER_DIMS)); + boolean selected = idx == selection; + entry.render(screen, pos.x, pos.y, selected); + if (selected && entry.isSelectable()) { + // draw the arrows + Font.draw("> ", screen, pos.x - Font.textWidth("> "), y, ListEntry.COL_SLCT); + Font.draw(" <", screen, pos.x + entry.getWidth(), y, ListEntry.COL_SLCT); + } + } + + y += ListEntry.getHeight() + spacing; + } + } + + void updateSelectedEntry(ListEntry newEntry) { updateEntry(selection, newEntry); } + void updateEntry(int idx, ListEntry newEntry) { + if(idx >= 0 && idx < entries.size()) + entries.set(idx, newEntry); + } + + public void removeSelectedEntry() { + entries.remove(selection); + + if(selection >= entries.size()) + selection = entries.size() - 1; + if(selection < 0) + selection = 0; + + doScroll(); + } + + public void setColors(Menu model) { + titleColor = model.titleColor; + } + + private void renderFrame(Screen screen) { + if(!hasFrame) return; + + int bottom = bounds.getBottom()-SpriteSheet.boxWidth; + int right = bounds.getRight()-SpriteSheet.boxWidth; + + for (int y = bounds.getTop(); y <= bottom; y += SpriteSheet.boxWidth) { // loop through the height of the bounds + for (int x = bounds.getLeft(); x <= right; x += SpriteSheet.boxWidth) { // loop through the width of the bounds + + boolean xend = x == bounds.getLeft() || x == right; + boolean yend = y == bounds.getTop() || y == bottom; + int spriteoffset = (xend && yend ? 0 : (yend ? 1 : (xend ? 2 : 3))); // determines which sprite to use + int mirrors = ( x == right ? 1 : 0 ) + ( y == bottom ? 2 : 0 ); // gets mirroring + + + screen.render(x, y, spriteoffset + 21 * 32, mirrors, 3); + + if(x < right && x + SpriteSheet.boxWidth > right) + x = right - SpriteSheet.boxWidth; + } + + if(y < bottom && y + SpriteSheet.boxWidth > bottom) + y = bottom - SpriteSheet.boxWidth; + } + } + + + + /// This needs to be in the Menu class, to have access to the private constructor and fields. + + public static class Builder { + + private static final Point center = new Point(Screen.w/2, Screen.h/2); + + private Menu menu; + + private boolean setSelectable = false; + private float padding = 1; + + @NotNull private RelPos titlePos = RelPos.TOP; + private boolean fullTitleColor = false, setTitleColor = false; + private int titleCol = Color.YELLOW; + + @NotNull private Point anchor = center; + @NotNull private RelPos menuPos = RelPos.CENTER; + private Dimension menuSize = null; + + public Builder(boolean hasFrame, int entrySpacing, RelPos entryPos, ListEntry... entries) { this(hasFrame, entrySpacing, entryPos, Arrays.asList(entries)); } + public Builder(boolean hasFrame, int entrySpacing, RelPos entryPos, List entries) { + menu = new Menu(); + setEntries(entries); + menu.hasFrame = hasFrame; + menu.spacing = entrySpacing; + menu.entryPos = entryPos; + } + + public Builder setEntries(ListEntry... entries) { return setEntries(Arrays.asList(entries)); } + public Builder setEntries(List entries) { + menu.entries.clear(); + menu.entries.addAll(entries); + return this; + } + + public Builder setPositioning(Point anchor, RelPos menuPos) { + this.anchor = anchor == null ? new Point() : anchor; + this.menuPos = menuPos == null ? RelPos.BOTTOM_RIGHT : menuPos; + return this; + } + + public Builder setSize(int width, int height) { menuSize = new Dimension(width, height); return this; } + public Builder setMenuSize(Dimension d) { menuSize = d; return this; } // can be used to set the size to null + + public Builder setBounds(Rectangle rect) { + menuSize = rect.getSize(); + setPositioning(rect.getCenter(), RelPos.CENTER); // because the anchor represents the center of the rectangle. + return this; + } + + public Builder setDisplayLength(int numEntries) { menu.displayLength = numEntries; return this; } + + + public Builder setTitlePos(RelPos rp) { titlePos = (rp == null ? RelPos.TOP : rp); return this; } + + public Builder setTitle(String title) { menu.title = title; return this; } + + public Builder setTitle(String title, int color) { return setTitle(title, color, false); } + public Builder setTitle(String title, int color, boolean fullColor) { + menu.title = title; + + fullTitleColor = fullColor; + setTitleColor = true; + if(fullColor) // this means that the color is the full 4 parts, abcd. Otherwise, it is assumed it is only the main component, the one that matters. + menu.titleColor = color; + else + titleCol = color; + + return this; + } + + public Builder setFrame(boolean hasFrame) { menu.hasFrame = hasFrame; return this; } + + + public Builder setScrollPolicies(float padding, boolean wrap) { + this.padding = padding; + menu.wrap = wrap; + return this; + } + + public Builder setShouldRender(boolean render) { menu.shouldRender = render; return this; } + + public Builder setSelectable(boolean selectable) { + setSelectable = true; + menu.selectable = selectable; + return this; + } + + public Builder setSelection(int sel) { menu.selection = sel; return this; } + public Builder setSelection(int sel, int dispSel) { + menu.selection = sel; + menu.dispSelection = dispSel; + return this; + } + + public Menu createMenu() { + // this way, I don't have to reference all the variables to a different var. + return copy().createMenu(this); + } + private Menu createMenu(Builder b) { + if(b == this) + return copy().createMenu(this); + + menu.title = Localization.getLocalized(menu.title); + + // set default selectability + if(!setSelectable) { + for(ListEntry entry: menu.entries) { + menu.selectable = menu.selectable || entry.isSelectable(); + if(menu.selectable) + break; + } + } + + // check the centering of the title, and find the dimensions of the title's display space. + + menu.drawVertically = titlePos == RelPos.LEFT || titlePos == RelPos.RIGHT; + + Dimension titleDim = menu.drawVertically ? + new Dimension(Font.textHeight()*2, Font.textWidth(menu.title)) : + new Dimension(Font.textWidth(menu.title), Font.textHeight()*2); + + // find the area used by the title and/or frame, that can't be used by the entries + + /* Create an Insets instance, and do the following... + * - if the menu is selectable, add 2 buffer spaces on the left and right, for the selection arrows. + * - if the menu has a frame, then add one buffer space to all 4 sides + * - if the menu has a title AND a frame, do nothing. + * - if the menu has a title and NO frame, add two spaces to whatever side the title is on + * + * Remember to set the title pos one space inside the left/right bounds, so it doesn't touch the frame corner. + * + * Starting with the entry size figured out, add the insets to get the total size. + * Starting with the menu size set, subtract the insets to get the entry size. + */ + + Insets border; + if(menu.hasFrame) + border = new Insets(SpriteSheet.boxWidth); // add frame insets + else { + border = new Insets(); + + // add title insets + if (menu.title.length() > 0 && titlePos != RelPos.CENTER) { + RelPos c = titlePos; + int space = SpriteSheet.boxWidth * 2; + if (c.yIndex == 0) + border.top = space; + else if (c.yIndex == 2) + border.bottom = space; + else if (c.xIndex == 0) // must be center left + border.left = space; + else if (c.xIndex == 2) // must be center right + border.right = space; + } + } + + if(menu.isSelectable()) { + // add spacing for selection cursors + border.left += SpriteSheet.boxWidth * 2; + border.right += SpriteSheet.boxWidth * 2; + } + + if(menu.wrap && menu.displayLength > 0) + menu.displayLength = Math.min(menu.displayLength, menu.entries.size()); + + // I have anchor and menu's relative position to it, and may or may not have size. + Dimension entrySize; + + if(menuSize == null) { + int width = titleDim.width; + for(ListEntry entry: menu.entries) { + int entryWidth = entry.getWidth(); + if(menu.isSelectable() && !entry.isSelectable()) + entryWidth = Math.max(0, entryWidth - SpriteSheet.boxWidth * 4); + width = Math.max(width, entryWidth); + } + + if(menu.displayLength > 0) { // has been set; use to determine entry bounds + int height = (ListEntry.getHeight() + menu.spacing) * menu.displayLength - menu.spacing; + + entrySize = new Dimension(width, height); + } + else { + // no set size; just keep going to the edges of the screen + + int maxHeight; + if(menuPos.yIndex == 0) // anchor is lowest down coordinate (highest y value) + maxHeight = anchor.y; + else if(menuPos.yIndex == 2) + maxHeight = Screen.h - anchor.y; + else // is centered; take the lowest value of the other two, and double it + maxHeight = Math.min(anchor.y, Screen.h - anchor.y) * 2; + + maxHeight -= border.top + border.bottom; // reserve border space + + int entryHeight = menu.spacing + ListEntry.getHeight(); + int totalHeight = entryHeight * menu.entries.size() - menu.spacing; + maxHeight = ((maxHeight + menu.spacing) / entryHeight) * entryHeight - menu.spacing; + + entrySize = new Dimension(width, Math.min(maxHeight, totalHeight)); + } + + menuSize = border.addTo(entrySize); + } + else // menuSize was set manually + entrySize = border.subtractFrom(menuSize); + + + // set default max display length (needs size first) + if(menu.displayLength <= 0 && menu.entries.size() > 0) + menu.displayLength = (entrySize.height + menu.spacing) / (ListEntry.getHeight() + menu.spacing); + + // based on the menu centering, and the anchor, determine the upper-left point from which to draw the menu. + menu.bounds = menuPos.positionRect(menuSize, anchor, new Rectangle()); // reset to a value that is actually useful to the menu + + menu.entryBounds = border.subtractFrom(menu.bounds); + + menu.titleLoc = titlePos.positionRect(titleDim, menu.bounds); + + if(titlePos.xIndex == 0 && titlePos.yIndex != 1) + menu.titleLoc.x += SpriteSheet.boxWidth; + if(titlePos.xIndex == 2 && titlePos.yIndex != 1) + menu.titleLoc.x -= SpriteSheet.boxWidth; + + // set the menu title color + if(menu.title.length() > 0) { + if(fullTitleColor) + menu.titleColor = titleCol; + else { + if (!setTitleColor) titleCol = menu.hasFrame ? Color.YELLOW : Color.WHITE; + menu.titleColor = titleCol; // make it match the frame color, or be transparent + } + } + + if(padding < 0) padding = 0; + if(padding > 1) padding = 1; + menu.padding = (int)Math.floor(padding * menu.displayLength / 2); + + // done setting defaults/values; return the new menu + + menu.init(); // any setup the menu does by itself right before being finished. + return menu; + } + + // returns a new Builder instance, that can be further modified to create another menu. + public Builder copy() { + Builder b = new Builder(menu.hasFrame, menu.spacing, menu.entryPos, menu.entries); + + b.menu = new Menu(menu); + + b.anchor = anchor == null ? null : new Point(anchor); + b.menuSize = menuSize == null ? null : new Dimension(menuSize); + b.menuPos = menuPos; + b.setSelectable = setSelectable; + b.padding = padding; + b.titlePos = titlePos; + b.fullTitleColor = fullTitleColor; + b.setTitleColor = setTitleColor; + b.titleCol = titleCol; + + return b; + } + } + + public String toString() { + return title+"-Menu["+bounds+"]"; + } +} diff --git a/src/minicraft/screen/MultiplayerDisplay.java b/src/minicraft/screen/MultiplayerDisplay.java new file mode 100644 index 0000000..535d7e0 --- /dev/null +++ b/src/minicraft/screen/MultiplayerDisplay.java @@ -0,0 +1,349 @@ +package minicraft.screen; + +import java.io.InputStream; + +import minicraft.core.Action; +import minicraft.core.Game; +import minicraft.core.io.InputHandler; +import minicraft.gfx.Color; +import minicraft.gfx.Ellipsis; +import minicraft.gfx.Ellipsis.SequentialEllipsis; +import minicraft.gfx.Font; +import minicraft.gfx.FontStyle; +import minicraft.gfx.Screen; +import minicraft.network.MinicraftClient; +import minicraft.saveload.Save; +import minicraft.screen.entry.RangeEntry; + +import com.mashape.unirest.http.HttpResponse; +import com.mashape.unirest.http.JsonNode; +import com.mashape.unirest.http.Unirest; +import com.mashape.unirest.http.async.Callback; +import com.mashape.unirest.http.exceptions.UnirestException; +import org.json.JSONObject; + +public class MultiplayerDisplay extends Display { + + private static String domain = "https://playminicraft.com"; + private static String apiDomain = domain+"/api"; + + public static String savedIP = ""; + public static String savedUUID = ""; + public static String savedUsername = ""; + private String email = ""; + + private String waitingMessage = "waiting..."; + private String loadingMessage = "nothing"; + private String errorMessage = ""; + + private String typing = email; + private boolean inputIsValid = false; + + private final RangeEntry connectTimeout = new RangeEntry("Timeout (sec) (0=none)", 0, 120, (int)Math.ceil(MinicraftClient.DEFAULT_CONNECT_TIMEOUT/1000f)); + + private boolean online = false; + private boolean typingEmail = true; + + private Ellipsis ellipsis = new SequentialEllipsis(); + + private enum State { + WAITING, LOGIN, ENTERIP, LOADING, ERROR + } + + private State curState; + + public MultiplayerDisplay() { this(true); } + public MultiplayerDisplay(boolean pingSite) { + Game.ISONLINE = true; + Game.ISHOST = false; + + if(savedUUID == null) savedUUID = ""; + if(email == null) email = ""; + if(savedUsername == null) savedUsername = ""; + + if(pingSite) + contactAccountServer(() -> {}); + } + + // this automatically sets the ipAddress. + public MultiplayerDisplay(String ipAddress) { + this(false); + contactAccountServer(() -> { + if(curState == State.ENTERIP) { // login was automatic + setWaitMessage("connecting to server"); + Game.client = new MinicraftClient(savedUsername,this, ipAddress); + } else + savedIP = ipAddress; // must login manually, so the ip address is saved for now. + }); + } + + private void contactAccountServer(Action sitePingCallback) { + setWaitMessage("testing connection"); + + Unirest.get(domain).asBinaryAsync(new Callback() { + @Override + public void completed(HttpResponse httpResponse) { + if(httpResponse.getStatus() == 200) + online = true; + else + System.err.println("warning: minicraft site ping returned status code " + httpResponse.getStatus()); + + if(savedUUID.length() > 0) { + // there is a previous login that can be used; check that it's valid + setWaitMessage("attempting log in"); + //if (Game.debug) System.out.println("fetching username for uuid"); + fetchName(savedUUID); + } + + if(curState == State.ERROR) + return; + + // at this point, the game is online, and either the player could log in automatically, or has to enter their + // email and password. + + if(savedUsername.length() == 0 || savedUUID.length() == 0) + curState = State.LOGIN; // the player must log in manually. + else { + typing = savedIP; + curState = State.ENTERIP; // the user has sufficient credentials; skip login phase + } + + sitePingCallback.act(); + } + + @Override + public void failed(UnirestException e) { + System.err.println("website ping failed: "+e.getMessage()); + if(!e.getMessage().equalsIgnoreCase("connection reset by peer")) + e.printStackTrace(); + cancelled(); + } + + @Override + public void cancelled() { + System.err.println("website ping cancelled."); + if(savedUsername.length() == 0 || savedUUID.length() == 0) { + // couldn't validate username, and can't enter offline mode b/c there is no username + setError("could not connect to playminicraft account server, but no login data saved; cannot enter offline mode.", false); + return; + } + + // there is a saved copy of the uuid and username of the last player; use it for offline mode. + curState = State.ENTERIP; + + sitePingCallback.act(); + } + }); + } + + @Override + public void tick(InputHandler input) { + switch(curState) { + case LOGIN: + typing = input.addKeyTyped(typing, null); + + inputIsValid = typing.length() != 0; + + if(input.getKey("select").clicked && inputIsValid) { + if(typingEmail) { + typingEmail = false; + email = typing; + //new Save(game); + typing = ""; + } else { + login(email, typing); // typing = password + typing = ""; + } + return; + } + break; + + case ENTERIP: + typing = input.addKeyTyped(typing, null); + connectTimeout.tick(input); + if(input.getKey("select").clicked) { + setWaitMessage("connecting to server"); + savedIP = typing; + new Thread(() -> { + Game.client = new MinicraftClient(savedUsername, this, typing, connectTimeout.getValue()*1000); // typing = ipAddress + if(Game.client.isConnected()) + new Save(); // write the saved ip to file + typing = ""; + }).start(); + return; + } else if(input.getKey("shift-escape").clicked) { + // logout + savedUUID = ""; + savedUsername = ""; + new Save(); // so the next time they start up the game to log in, it won't try to log in automatically. + typing = ""; + curState = State.LOGIN; + } + break; + + case WAITING: + /// this is just in case something gets set too early or something and the error state is overridden. + if(errorMessage.length() > 0) + curState = State.ERROR; + break; + } + + if(input.getKey("exit").clicked && !Game.ISHOST) { + Game.setMenu(new TitleDisplay()); + } + } + + private void login(String email, String password) { + setWaitMessage("logging in"); + + /// HTTP REQUEST - send username and password to server via HTTPS, expecting a UUID in return. + Unirest.post(apiDomain+"/login") + .field("email", email) + .field("password", password) + .asJsonAsync(new Callback() { + @Override + public void completed(HttpResponse response) { + JSONObject json = response.getBody().getObject(); + if(Game.debug) System.out.println("received json from login attempt: " + json.toString()); + switch(json.getString("status")) { + case "error": + setError(json.getString("message"), false); // in case the user abandoned the menu, don't drag them back. + break; + + case "success": + savedUUID = json.getString("uuid"); + savedUsername = json.getString("name"); + setWaitMessage("saving credentials"); + new Save(); + typing = savedIP; + curState = State.ENTERIP; + break; + } + } + + @Override + public void failed(UnirestException e) { + e.printStackTrace(); + cancelled(); + } + + @Override + public void cancelled() { + setError("login failed.", false); + } + }); + } + + private void fetchName(String uuid) { + /// HTTP REQUEST - ATTEMPT TO SEND UUID TO SERVER AND UPDATE USERNAME + HttpResponse response = null; + + try { + response = Unirest.post(apiDomain+"/fetch-name") + .field("uuid", savedUUID) + .asJson(); + } catch (UnirestException e) { + e.printStackTrace(); + } + + if(response != null) { + JSONObject json = response.getBody().getObject(); + //if(Game.debug) System.out.println("received json from username request: " + json.toString()); + switch(json.getString("status")) { + case "error": + setError("problem with saved login data; please exit and login again.", false); + savedUUID = ""; + break; + + case "success": + if(Game.debug) System.out.println("successfully received username from playminicraft server"); + savedUsername = json.getString("name"); + break; + } + } else// if(Game.debug) + setError("Internal server error: Couldn't fetch username from uuid"); + //System.out.println("response to username fetch was null"); + } + + + public void setWaitMessage(String message) { + waitingMessage = message; + curState = State.WAITING; + } + + public void setLoadingMessage(String msg) { + curState = State.LOADING; + loadingMessage = msg; + } + + public void setError(String msg) { setError(msg, true); } + private void setError(String msg, boolean overrideMenu) { + if(curState == State.ERROR) return; // keep original message + this.curState = State.ERROR; + errorMessage = msg; + if(overrideMenu && Game.getMenu() != this && Game.isValidClient()) + Game.setMenu(this); // if you don't override the menu, then you'd have to already be viewing the multiplayer menu. + } + + @Override + public void render(Screen screen) { + screen.clear(0); + + switch(curState) { + case ENTERIP: + Font.drawCentered("logged in as: " + savedUsername, screen, 6, Color.get(1, 102, 255, 102)); + + if(!online) + Font.drawCentered("offline mode: local servers only", screen, Screen.h/2 - Font.textHeight()*6, Color.get(1, 153, 153, 255)); + + Font.drawCentered("Enter ip address to connect to:", screen, Screen.h/2-Font.textHeight()*2-2, Color.get(1, 255)); + Font.drawCentered(typing, screen, Screen.h/2-Font.textHeight(), Color.get(1, 255, 255, 102)); + + connectTimeout.render(screen, (Screen.w-connectTimeout.getWidth()) / 2, Screen.h/2+Font.textHeight()*2, true); + + Font.drawCentered("Press Shift-Escape to logout", screen, Screen.h-Font.textHeight()*7, Color.get(1, 204)); + break; + + case LOGIN: + String msg = "Enter email:"; + if(!typingEmail) + msg = "Enter password:"; + Font.drawCentered(msg, screen, Screen.h/2-6, Color.WHITE); + + msg = typing; + if(!typingEmail) + //noinspection ReplaceAllDot + msg = msg.replaceAll(".", "."); + Font.drawCentered(msg, screen, Screen.h/2+6, (inputIsValid?Color.get(1, 204):Color.RED)); + if(!inputIsValid) { + Font.drawCentered("field is blank", screen, Screen.h/2+20, Color.RED); + } + + Font.drawCentered("get an account at:", screen, Font.textHeight()/2-1, Color.get(1, 153, 204, 255)); + Font.drawCentered(domain.substring(domain.indexOf("://")+3)+"/register", screen, Font.textHeight()*3/2, Color.get(1, 153, 204, 255)); + + break; + + case WAITING: + Font.drawCentered(waitingMessage+ ellipsis.updateAndGet(), screen, Screen.h/2, Color.WHITE); + break; + + case LOADING: + Font.drawCentered("Loading "+loadingMessage+" from server"+ ellipsis.updateAndGet(), screen, Screen.h/2, Color.WHITE); + //Font.drawCentered(transferPercent+"%", screen, Screen.h/2+6, Color.WHITE); + break; + + case ERROR: + //if(Updater.tickCount % 10 == 0) System.out.println("error message: " + errorMessage); + Font.drawCentered("Could not connect to server:", screen, Screen.h/2-6, Color.RED); + FontStyle style = new FontStyle(Color.get(1, 255, 51, 51)).setYPos(Screen.h/2+6); + Font.drawParagraph(errorMessage, screen, style, 1); + //Font.drawCentered(errorMessage, screen, Screen.h/2+6, Color.get(1, 255, 51, 51)); + break; + } + + if(curState == State.ENTERIP || curState == State.ERROR) { + Font.drawCentered("Press "+Game.input.getMapping("exit")+" to return", screen, Screen.h-Font.textHeight()*2, Color.GRAY); + } + } +} diff --git a/src/minicraft/screen/OptionsDisplay.java b/src/minicraft/screen/OptionsDisplay.java new file mode 100644 index 0000000..a8ba228 --- /dev/null +++ b/src/minicraft/screen/OptionsDisplay.java @@ -0,0 +1,33 @@ +package minicraft.screen; + +import minicraft.core.Game; +import minicraft.core.io.Localization; +import minicraft.core.io.Settings; +import minicraft.saveload.Save; +import minicraft.screen.entry.SelectEntry; + +public class OptionsDisplay extends Display { + + public OptionsDisplay() { + super(true, new Menu.Builder(false, 6, RelPos.LEFT, + Settings.getEntry("diff"), + Settings.getEntry("fps"), + Settings.getEntry("sound"), + Settings.getEntry("autosave"), + Settings.getEntry("skinon"), + new SelectEntry("Change Key Bindings", () -> Game.setMenu(new KeyInputDisplay())), + Settings.getEntry("language"), + Settings.getEntry("textures") + ) + .setTitle("Options") + .createMenu() + ); + } + + @Override + public void onExit() { + Localization.changeLanguage((String)Settings.get("language")); + new Save(); + Game.MAX_FPS = (int)Settings.get("fps"); + } +} diff --git a/src/minicraft/screen/PauseDisplay.java b/src/minicraft/screen/PauseDisplay.java new file mode 100644 index 0000000..6175ad3 --- /dev/null +++ b/src/minicraft/screen/PauseDisplay.java @@ -0,0 +1,106 @@ +package minicraft.screen; + +import java.util.ArrayList; +import java.util.Arrays; + +import minicraft.core.Game; +import minicraft.core.MyUtils; +import minicraft.core.Network; +import minicraft.core.io.InputHandler; +import minicraft.core.io.Localization; +import minicraft.gfx.Color; +import minicraft.saveload.Save; +import minicraft.screen.entry.BlankEntry; +import minicraft.screen.entry.ListEntry; +import minicraft.screen.entry.SelectEntry; +import minicraft.screen.entry.StringEntry; + +public class PauseDisplay extends Display { + + public PauseDisplay() { + String upString = Game.input.getMapping("cursor-up")+ Localization.getLocalized(" and ")+Game.input.getMapping("cursor-down")+Localization.getLocalized(" to Scroll"); + String selectString = Game.input.getMapping("select")+Localization.getLocalized(": Choose"); + + + ArrayList entries = new ArrayList<>(); + entries.addAll(Arrays.asList( + new BlankEntry(), + new SelectEntry("Return to Game", () -> Game.setMenu(null)), + new SelectEntry("Options", () -> Game.setMenu(new OptionsDisplay())) + )); + + if(!Game.ISONLINE) { + entries.add(new SelectEntry("Make World Multiplayer", () -> { + Game.setMenu(null); + Network.startMultiplayerServer(); + })); + } + + if(!Game.isValidClient()) { + entries.add(new SelectEntry("Save Game", () -> { + Game.setMenu(null); + if(!Game.isValidServer()) + new Save(WorldSelectDisplay.getWorldName()); + else + Game.server.saveWorld(); + })); + } + + entries.addAll(Arrays.asList( + new SelectEntry("Main Menu", () -> { + ArrayList items = new ArrayList<>(); + items.addAll(Arrays.asList(StringEntry.useLines( + "Are you sure you want to", + MyUtils.fromNetworkStatus("Exit the Game?", "Leave the Server?", "Close the Server?") + ))); + + if(!Game.isValidServer()) { + int color = MyUtils.fromNetworkStatus(Color.RED, Color.GREEN, Color.TRANSPARENT); + items.addAll(Arrays.asList(StringEntry.useLines(color, "", + MyUtils.fromNetworkStatus("All unsaved progress", "Your progress", ""), + MyUtils.fromNetworkStatus("will be lost!", "will be saved.", ""), + "" + ))); + } + + items.add(new BlankEntry()); + items.add(new SelectEntry(Game.isValidServer()?"Cancel":"No", Game::exitMenu)); + + if(Game.isValidServer()) + items.add(new SelectEntry("Save and Quit", () -> { + Game.setMenu(new LoadingDisplay()); + new Save(WorldSelectDisplay.getWorldName()); + Game.setMenu(new TitleDisplay()); + })); + + items.add(new SelectEntry(Game.isValidServer()?"Quit without saving":"Yes", () -> Game.setMenu(new TitleDisplay()))); + + Game.setMenu(new Display(false, true, new Menu.Builder(true, 8, RelPos.CENTER, items + ).createMenu())); + }), + + new BlankEntry(), + + new StringEntry(upString, Color.GRAY), + new StringEntry(selectString, Color.GRAY) + )); + + menus = new Menu[] { + new Menu.Builder(true, 4, RelPos.CENTER, entries) + .setTitle("Paused", Color.YELLOW) + .createMenu() + }; + } + + @Override + public void init(Display parent) { + super.init(null); // ignore; pause menus always lead back to the game + } + + @Override + public void tick(InputHandler input) { + super.tick(input); + if (input.getKey("pause").clicked) + Game.exitMenu(); + } +} diff --git a/src/minicraft/screen/PlayerDeathDisplay.java b/src/minicraft/screen/PlayerDeathDisplay.java new file mode 100644 index 0000000..3b812bb --- /dev/null +++ b/src/minicraft/screen/PlayerDeathDisplay.java @@ -0,0 +1,50 @@ +package minicraft.screen; + +import java.util.ArrayList; +import java.util.Arrays; + +import minicraft.core.Game; +import minicraft.core.World; +import minicraft.core.io.Settings; +import minicraft.gfx.Point; +import minicraft.gfx.SpriteSheet; +import minicraft.screen.entry.BlankEntry; +import minicraft.screen.entry.ListEntry; +import minicraft.screen.entry.SelectEntry; +import minicraft.screen.entry.StringEntry; + +public class PlayerDeathDisplay extends Display { + // this is an IMPORTANT bool, determines if the user should respawn or not. :) + public static boolean shouldRespawn = true; + + public PlayerDeathDisplay() { + super(false, false); + + ArrayList entries = new ArrayList<>(); + entries.addAll(Arrays.asList( + new StringEntry("Time: " + InfoDisplay.getTimeString()), + new StringEntry("Score: " + Game.player.getScore()), + new BlankEntry() + )); + + if(!Settings.get("mode").equals("hardcore")) { + entries.add(new SelectEntry("Respawn", () -> { + World.resetGame(); + if (!Game.isValidClient()) + Game.setMenu(null); //sets the menu to nothing + })); + } + + if(Settings.get("mode").equals("hardcore") || !Game.isValidClient()) + entries.add(new SelectEntry("Quit", () -> Game.setMenu(new TitleDisplay()))); + + menus = new Menu[]{ + new Menu.Builder(true, 0, RelPos.LEFT, entries) + .setPositioning(new Point(SpriteSheet.boxWidth, SpriteSheet.boxWidth * 3), RelPos.BOTTOM_RIGHT) + .setTitle("You died! Aww!") + .setTitlePos(RelPos.TOP_LEFT) + .createMenu() + }; + + } +} diff --git a/src/minicraft/screen/PlayerInvDisplay.java b/src/minicraft/screen/PlayerInvDisplay.java new file mode 100644 index 0000000..7f1292a --- /dev/null +++ b/src/minicraft/screen/PlayerInvDisplay.java @@ -0,0 +1,30 @@ +package minicraft.screen; + +import minicraft.core.Game; +import minicraft.core.io.InputHandler; +import minicraft.entity.mob.Player; + +public class PlayerInvDisplay extends Display { + + private Player player; + + public PlayerInvDisplay(Player player) { + super(new InventoryMenu(player, player.getInventory(), "Inventory")); + this.player = player; + } + + @Override + public void tick(InputHandler input) { + super.tick(input); + + if(input.getKey("menu").clicked) { + Game.exitMenu(); + return; + } + + if(input.getKey("attack").clicked && menus[0].getNumOptions() > 0) { + player.activeItem = player.getInventory().remove(menus[0].getSelection()); + Game.exitMenu(); + } + } +} diff --git a/src/minicraft/screen/RecipeMenu.java b/src/minicraft/screen/RecipeMenu.java new file mode 100644 index 0000000..910bdcb --- /dev/null +++ b/src/minicraft/screen/RecipeMenu.java @@ -0,0 +1,29 @@ +package minicraft.screen; + +import java.util.List; + +import minicraft.entity.mob.Player; +import minicraft.item.Recipe; +import minicraft.screen.entry.RecipeEntry; + +class RecipeMenu extends ItemListMenu { + + private static RecipeEntry[] getAndSortRecipes(List recipes, Player player) { + recipes.sort((r1, r2) -> { + boolean craft1 = r1.checkCanCraft(player); + boolean craft2 = r2.checkCanCraft(player); + if(craft1 == craft2) + return 0; + if(craft1) return -1; + if(craft2) return 1; + + return 0; // should never actually be reached + }); + + return RecipeEntry.useRecipes(recipes); + } + + RecipeMenu(List recipes, String title, Player player) { + super(getAndSortRecipes(recipes, player), title); + } +} diff --git a/src/minicraft/screen/RelPos.java b/src/minicraft/screen/RelPos.java new file mode 100644 index 0000000..83af15c --- /dev/null +++ b/src/minicraft/screen/RelPos.java @@ -0,0 +1,66 @@ +package minicraft.screen; + +import minicraft.core.MyUtils; +import minicraft.gfx.Dimension; +import minicraft.gfx.Point; +import minicraft.gfx.Rectangle; + +// stands for "Relative Position" +public enum RelPos { + TOP_LEFT, TOP, TOP_RIGHT, + LEFT, CENTER, RIGHT, + BOTTOM_LEFT, BOTTOM, BOTTOM_RIGHT; + + public int xIndex, yIndex; + + // I think this way, the enums will all be constructed before this gets called, so there won't be any mishaps with number of values. + static { + for(RelPos rp: RelPos.values()) { + int ord = rp.ordinal(); + rp.xIndex = ord % 3; + rp.yIndex = ord / 3; + } + } + + public static RelPos getPos(int xIndex, int yIndex) { + return values()[MyUtils.clamp(xIndex, 0, 2) + MyUtils.clamp(yIndex, 0, 2)*3]; + } + + public RelPos getOpposite() { + int nx = -(xIndex-1) + 1; + int ny = -(yIndex-1) + 1; + return getPos(nx, ny); + } + + /** positions the given rect around the given anchor. The double size is what aligns it to a point rather than a rect. */ + public Point positionRect(Dimension rectSize, Point anchor) { + Rectangle bounds = new Rectangle(anchor.x, anchor.y, rectSize.width*2, rectSize.height*2, Rectangle.CENTER_DIMS); + return positionRect(rectSize, bounds); + } + // the point is returned as a rectangle with the given dimension and the found location, within the provided dummy rectangle. + public Rectangle positionRect(Dimension rectSize, Point anchor, Rectangle dummy) { + Point pos = positionRect(rectSize, anchor); + dummy.setSize(rectSize, RelPos.TOP_LEFT); + dummy.setPosition(pos, RelPos.TOP_LEFT); + return dummy; + } + + /** positions the given rect to a relative position in the container. */ + public Point positionRect(Dimension rectSize, Rectangle container) { + Point tlcorner = container.getCenter(); + + // this moves the inner box correctly + tlcorner.x += ((xIndex -1) * container.getWidth() / 2) - (xIndex * rectSize.width / 2); + tlcorner.y += ((yIndex -1) * container.getHeight() / 2) - (yIndex * rectSize.height / 2); + + return tlcorner; + } + + // the point is returned as a rectangle with the given dimension and the found location, within the provided dummy rectangle. + public Rectangle positionRect(Dimension rectSize, Rectangle container, Rectangle dummy) { + Point pos = positionRect(rectSize, container); + dummy.setSize(rectSize, RelPos.TOP_LEFT); + dummy.setPosition(pos, RelPos.TOP_LEFT); + return dummy; + } +} diff --git a/src/minicraft/screen/TempDisplay.java b/src/minicraft/screen/TempDisplay.java new file mode 100644 index 0000000..be38360 --- /dev/null +++ b/src/minicraft/screen/TempDisplay.java @@ -0,0 +1,49 @@ +package minicraft.screen; + +import minicraft.core.Game; +import minicraft.core.MyUtils; + +public class TempDisplay extends Display { + + private int milliDelay; + + public TempDisplay(int milliDelay) { + this.milliDelay = milliDelay; + } + + public TempDisplay(int milliDelay, Menu... menus) { + super(menus); + this.milliDelay = milliDelay; + } + + public TempDisplay(int milliDelay, boolean clearScreen) { + super(clearScreen); + this.milliDelay = milliDelay; + } + + public TempDisplay(int milliDelay, boolean clearScreen, Menu... menus) { + super(clearScreen, menus); + this.milliDelay = milliDelay; + } + + public TempDisplay(int milliDelay, boolean clearScreen, boolean canExit) { + super(clearScreen, canExit); + this.milliDelay = milliDelay; + } + + public TempDisplay(int milliDelay, boolean clearScreen, boolean canExit, Menu... menus) { + super(clearScreen, canExit, menus); + this.milliDelay = milliDelay; + } + + @Override + public void init(Display parent) { + super.init(parent); + + new Thread(() -> { + MyUtils.sleep(milliDelay); + if(Game.getMenu() == TempDisplay.this) + Game.exitMenu(); + }).start(); + } +} diff --git a/src/minicraft/screen/TitleDisplay.java b/src/minicraft/screen/TitleDisplay.java new file mode 100644 index 0000000..95d5193 --- /dev/null +++ b/src/minicraft/screen/TitleDisplay.java @@ -0,0 +1,284 @@ +package minicraft.screen; + +import java.time.LocalDateTime; +import java.time.Month; +import java.util.Random; + +import minicraft.core.Game; +import minicraft.core.Network; +import minicraft.core.Renderer; +import minicraft.core.VersionInfo; +import minicraft.core.World; +import minicraft.core.io.InputHandler; +import minicraft.entity.mob.RemotePlayer; +import minicraft.gfx.Color; +import minicraft.gfx.Font; +import minicraft.gfx.Point; +import minicraft.gfx.Screen; +import minicraft.level.Level; +import minicraft.screen.entry.BlankEntry; +import minicraft.screen.entry.LinkEntry; +import minicraft.screen.entry.ListEntry; +import minicraft.screen.entry.SelectEntry; +import minicraft.screen.entry.StringEntry; + +import org.jetbrains.annotations.NotNull; + +public class TitleDisplay extends Display { + private static final Random random = new Random(); + + private int rand; + private int count = 0; // this and reverse are for the logo; they produce the fade-in/out effect. + private boolean reverse = false; + + private Object timer; + + public TitleDisplay() { + super(true, false, new Menu.Builder(false, 2, RelPos.CENTER, + new BlankEntry(), + new BlankEntry(), + new SelectEntry("Play", () -> { + if(WorldSelectDisplay.getWorldNames().size() > 0) + Game.setMenu(new Display(true, new Menu.Builder(false, 2, RelPos.CENTER, + new SelectEntry("Load World", () -> Game.setMenu(new WorldSelectDisplay())), + new SelectEntry("New World", () -> Game.setMenu(new WorldGenDisplay())) + ).createMenu())); + else + Game.setMenu(new WorldGenDisplay()); + }), + new SelectEntry("Join Online World", () -> Game.setMenu(new MultiplayerDisplay())), + new SelectEntry("Options", () -> Game.setMenu(new OptionsDisplay())), + displayFactory("Help", + new SelectEntry("Instructions", () -> Game.setMenu(new BookDisplay(BookData.instructions))), + new BlankEntry(), + new SelectEntry("Storyline Guide (for the weak)", () -> Game.setMenu(new BookDisplay(BookData.storylineGuide))), + new BlankEntry(), + new SelectEntry("About", () -> Game.setMenu(new BookDisplay(BookData.about))) + ), + new SelectEntry("Quit", Game::quit) + ) + .setPositioning(new Point(Screen.w/2, Screen.h*3/5), RelPos.CENTER) + .createMenu() + ); + } + + @Override + public void init(Display parent) { + super.init(null); // The TitleScreen never has a parent. + Renderer.readyToRenderGameplay = false; + + // check version + checkVersion(); + + /// this is useful to just ensure that everything is really reset as it should be. + if (Game.server != null) { + if (Game.debug) System.out.println("wrapping up loose server ends"); + Game.server.endConnection(); + Game.server = null; + } + if (Game.client != null) { + if (Game.debug) System.out.println("wrapping up loose client ends"); + Game.client.endConnection(); + Game.client = null; + } + Game.ISONLINE = false; + + LocalDateTime time = LocalDateTime.now(); + if (time.getMonth() == Month.DECEMBER) { + if (time.getDayOfMonth() == 19) rand = 1; + if (time.getDayOfMonth() == 25) rand = 2; + } else { + rand = random.nextInt(splashes.length - 3) + 3; + } + + World.levels = new Level[World.levels.length]; + + if(Game.player == null || Game.player instanceof RemotePlayer) + // was online, need to reset player + World.resetGame(false); + } + + private void checkVersion() { + VersionInfo latestVersion = Network.getLatestVersion(); + if(latestVersion == null) { + Network.findLatestVersion(this::checkVersion); + } + else { + if(Game.debug) System.out.println("latest version = "+latestVersion.version); + if(latestVersion.version.compareTo(Game.VERSION) > 0) { // link new version + menus[0].updateEntry(0, new StringEntry("New: "+latestVersion.releaseName, Color.GREEN)); + menus[0].updateEntry(1, new LinkEntry(Color.CYAN, "--Select here to Download--", latestVersion.releaseUrl, "Direct link to latest version: " + latestVersion.releaseUrl + "\nCan also be found here with change log: https://www.github.com/chrisj42/minicraft-plus-revived/releases")); + } + else if(latestVersion.releaseName.length() > 0) + menus[0].updateEntry(0, new StringEntry("You have the latest version.", Color.DARK_GRAY)); + else + menus[0].updateEntry(0, new StringEntry("Connection failed, could not check for updates.", Color.RED)); + } + } + + @NotNull + private static SelectEntry displayFactory(String entryText, ListEntry... entries) { + return new SelectEntry(entryText, () -> Game.setMenu(new Display(true, new Menu.Builder(false, 2, RelPos.CENTER, entries).createMenu()))); + } + + @Override + public void tick(InputHandler input) { + if (input.getKey("r").clicked) rand = random.nextInt(splashes.length - 3) + 3; + + if (!reverse) { + count++; + if (count == 25) reverse = true; + } else { + count--; + if (count == 0) reverse = false; + } + + super.tick(input); + } + + @Override + public void render(Screen screen) { + super.render(screen); + + int h = 6; // Height of squares (on the spritesheet) + int w = 20; // Width of squares (on the spritesheet) + int xo = (Screen.w - w * 8) / 2; // X location of the title + int yo = 22; // Y location of the title + + for (int y = 0; y < h; y++) { + for (int x = 0; x < w; x++) { + screen.render(xo + x * 8, yo + y * 8, x + (y + 7) * 32, 0, 3); + } + } + + boolean isblue = splashes[rand].contains("blue"); + boolean isGreen = splashes[rand].contains("Green"); + boolean isRed = splashes[rand].contains("Red"); + + /// this isn't as complicated as it looks. It just gets a color based off of count, which oscilates between 0 and 25. + int bcol = 5 - count / 5; // this number ends up being between 1 and 5, inclusive. + int splashColor = isblue ? Color.BLUE : isRed ? Color.RED : isGreen ? Color.GREEN : Color.get(1, bcol*51, bcol*51, bcol*25); + + + Font.drawCentered(splashes[rand], screen, 74, splashColor); + + Font.draw("Version " + "0.02a", screen, 1, 1, Color.get(1, 51)); + } + + private static final String[] splashes = { + "Secret Splash!", + "Happy birthday Minicraft!", + "Happy XMAS!", + "Multiplayer Now Included!", + // "Also play InfinityTale!", + // "Also play Minicraft Deluxe!", + // "Also play Alecraft!", + // "Also play Hackcraft!", + // "Also play MiniCrate!", + // "Also play MiniCraft Mob Overload!", + "Now with better fishing!", + "Now with better tools!", + "Now with better chests!", + "Now with better dungeons!", + "Only on PlayMinicraft.com!", + "Playminicraft.com is the bomb!", + // "@MinicraftPlus on Twitter", + "MinicraftPlus on Youtube", + //"Join the Forums!", + "The Wiki is weak! Help it!", + "Notch is Awesome!", + "Dillyg10 is cool as Ice!", + "Shylor is the man!", + "Chris J is great with portals!", + "AntVenom loves cows! Honest!", + "You should read Antidious Venomi!", + "Oh Hi Mark", + "Use the force!", + "Keep calm!", + "Get him, Steve!", + "Forty-Two!", + "Kill Creeper, get Gunpowder!", + "Kill Cow, get Beef!", + "Kill Zombie, get Cloth!", + "Kill Slime, get Slime!", + "Kill Skeleton, get Bones!", + "Kill Sheep, get Wool!", + "Kill Pig, get Porkchop!", + "Gold > Iron", + "Gem > Gold", + "Test == InDev!", + "Story? Uhh...", + "Infinite terrain? What's that?", + "Redstone? What's that?", + "Minecarts? What are those?", + "Windows? I prefer Doors!", + "2.5D FTW!", + "3rd dimension not included!", + "Null not included", + "Mouse not included!", + "No spiders included!", + "No Endermen included!", + "No chickens included!", + "Grab your friends!", + "Creepers included!", + "Skeletons included!", + "Knights included!", + "Snakes included!", + "Cows included!", + "Sheep included!", + "Pigs included!", + "Bigger Worlds!", + "World types!", + "World themes!", + "Sugarcane is a Idea!", + "Milk is an idea!", + "Creeper, aw man", + "So we back in the mine,", + "pickaxe swinging from side to side", + "In search of Gems!", + "Life itself suspended by a thread", + "saying ay-oh, that creeper's KO'd!", + "Gimmie a bucket!", + "Farming with water!", + "Press \"R\"!", + "Get the High-Score!", + "Potions ftw!", + "Beds ftw!", + "Defeat the Air Wizard!", + "Conquer the Dungeon!", + "One down, one to go...", + "Loom + Wool = String!", + "String + Wood = Rod!", + "Sand + Gunpowder = TNT!", + "Sleep at Night!", + "Farm at Day!", + "Explanation Mark!", + "!sdrawkcab si sihT", + "This is forwards!", + "Why is this blue?", + "Green is a nice color!", + "Red is my favorite color!", + "Y U NO BOAT!?", + "Made with 10000% Vitamin Z!", + "Too much DP!", + "Punch the Moon!", + "This is String qq!", + "Why?", + //"You are null!", + "hello down there!", + "That guy is such a sly fox!", + "Hola senor!", + "Sonic Boom!", + "Hakuna Matata!", + "One truth prevails!", + "Awesome!", + "Sweet!", + "Great!", + "Cool!", + "Radical!", + "011011000110111101101100!", + "001100010011000000110001!", + "011010000110110101101101?", + "...zzz...", + }; +} diff --git a/src/minicraft/screen/WorldEditDisplay.java b/src/minicraft/screen/WorldEditDisplay.java new file mode 100644 index 0000000..ab46444 --- /dev/null +++ b/src/minicraft/screen/WorldEditDisplay.java @@ -0,0 +1,120 @@ +package minicraft.screen; + +import java.io.File; +import java.io.IOException; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; + +import minicraft.core.FileHandler; +import minicraft.core.Game; +import minicraft.core.io.InputHandler; +import minicraft.gfx.Color; +import minicraft.screen.Menu.Builder; +import minicraft.screen.WorldSelectDisplay.Action; +import minicraft.screen.entry.InputEntry; +import minicraft.screen.entry.ListEntry; +import minicraft.screen.entry.StringEntry; + +public class WorldEditDisplay extends Display { + + /// this class will be used to enact the extra actions (copy, delete, rename) that you can do for worlds in the WorldSelectMenu. + + private Action action; + private String worldName; + + private static final String worldsDir = Game.gameDir + "/saves/"; + + public WorldEditDisplay(Action action, String worldName) { + super(true); + this.action = action; + this.worldName = worldName; + + Builder builder = new Builder(false, 8, RelPos.CENTER); + ArrayList entries = new ArrayList<>(); + + if(action != Action.Delete) { + List names = WorldSelectDisplay.getWorldNames(); + if(action == Action.Rename) + names.remove(worldName); + entries.add(new StringEntry("New World Name:", action.color)); + entries.add(WorldGenDisplay.makeWorldNameInput("", names, worldName)); + } else { + entries.addAll(Arrays.asList( + new StringEntry("Are you sure you want to delete", action.color), + new StringEntry("\""+worldName+"\"?", Color.tint(action.color, 1, true)), + new StringEntry("This can not be undone!", action.color) + )); + } + + entries.addAll(Arrays.asList(StringEntry.useLines(Color.WHITE, "", + Game.input.getMapping("select")+" to confirm", + Game.input.getMapping("exit")+" to cancel" + ))); + + builder.setEntries(entries); + + menus = new Menu[] {builder.createMenu()}; + } + + @Override + public void tick(InputHandler input) { + super.tick(input); + + if(input.getKey("select").clicked) { + // do action + InputEntry entry; + File world = new File(worldsDir + worldName); + switch (action) { + case Delete: + if(Game.debug) System.out.println("deleting world: " + world); + File[] list = world.listFiles(); + for (int i = 0; i < list.length; i++) { + list[i].delete(); + } + world.delete(); + + WorldSelectDisplay.refreshWorldNames(); + if(WorldSelectDisplay.getWorldNames().size() > 0) + Game.setMenu(new WorldSelectDisplay()); + else + Game.setMenu(new TitleDisplay()); + break; + + case Copy: + entry = (InputEntry)menus[0].getCurEntry(); + if(!entry.isValid()) + break; + //user hits enter with a valid new name; copy is created here. + String oldname = worldName; + String newname = entry.getUserInput(); + File newworld = new File(worldsDir + newname); + newworld.mkdirs(); + if (Game.debug) System.out.println("copying world " + world + " to world " + newworld); + // walk file tree + try { + FileHandler.copyFolderContents(new File(worldsDir+oldname).toPath(), newworld.toPath(), FileHandler.REPLACE_EXISTING, false); + } catch (IOException e) { + e.printStackTrace(); + } + + Game.setMenu(new WorldSelectDisplay()); + + break; + + case Rename: + entry = (InputEntry)menus[0].getCurEntry(); + if(!entry.isValid()) + break; + //user hits enter with a vaild new name; name is set here: + String name = entry.getUserInput(); + if (Game.debug) System.out.println("renaming world " + world + " to new name: " + name); + world.renameTo(new File(worldsDir + name)); + Game.setMenu(new WorldSelectDisplay()); + break; + } + } + // Display class will take care to exiting + } + +} diff --git a/src/minicraft/screen/WorldGenDisplay.java b/src/minicraft/screen/WorldGenDisplay.java new file mode 100644 index 0000000..5196484 --- /dev/null +++ b/src/minicraft/screen/WorldGenDisplay.java @@ -0,0 +1,114 @@ +package minicraft.screen; + +import java.util.Arrays; +import java.util.HashSet; +import java.util.List; +import java.util.Random; + +import minicraft.core.Game; +import minicraft.core.io.Localization; +import minicraft.core.io.Settings; +import minicraft.gfx.Color; +import minicraft.gfx.Font; +import minicraft.gfx.Screen; +import minicraft.screen.entry.InputEntry; +import minicraft.screen.entry.SelectEntry; + +public class WorldGenDisplay extends Display { + + private static final String worldNameRegex = "[a-zA-Z0-9 ]+"; + + private static InputEntry worldSeed = new InputEntry("World Seed", "[0-9]", 20); + + public static long getSeed() { + String seedStr = worldSeed.getUserInput(); + if(seedStr.length() == 0) + return new Random().nextLong(); + else + return Long.parseLong(seedStr); + } + + public static InputEntry makeWorldNameInput(String prompt, List takenNames, String initValue) { + return new InputEntry(prompt, worldNameRegex, 36, initValue) { + @Override + public boolean isValid() { + if(!super.isValid()) return false; + String name = getUserInput(); + for(String other: takenNames) + if(other.equalsIgnoreCase(name)) + return false; + + return true; + } + + @Override + public String getUserInput() { + return super.getUserInput().toLowerCase(Localization.getSelectedLocale()); + } + }; + } + + public WorldGenDisplay() { + super(true); + + InputEntry nameField = makeWorldNameInput("Enter World Name", WorldSelectDisplay.getWorldNames(), ""); + + SelectEntry nameHelp = new SelectEntry("Trouble with world name?", () -> Game.setMenu(new BookDisplay("it seems you've set letters as the controls to move the cursor up and down, which is probably annoying. This can be changed in the key binding menu as the \"cursor-XXX\" keys. For now, to type the letter instead of moving the cursor, hold the shift key while typing."))) { + @Override + public int getColor(boolean isSelected) { + return Color.get(1, 204); + } + }; + + nameHelp.setVisible(false); + + HashSet controls = new HashSet<>(); + controls.addAll(Arrays.asList(Game.input.getMapping("cursor-up").split("/"))); + controls.addAll(Arrays.asList(Game.input.getMapping("cursor-down").split("/"))); + for(String key: controls) { + if(key.matches("^\\w$")) { + nameHelp.setVisible(true); + break; + } + } + + worldSeed = new InputEntry("World Seed", "[0-9]+", 20) { + @Override + public boolean isValid() { return true; } + }; + + menus = new Menu[] { + new Menu.Builder(false, 10, RelPos.LEFT, + nameField, + nameHelp, + Settings.getEntry("mode"), + Settings.getEntry("scoretime"), + + new SelectEntry("Create World", () -> { + if(!nameField.isValid()) return; + WorldSelectDisplay.setWorldName(nameField.getUserInput(), false); + Game.setMenu(new LoadingDisplay()); + }) { + @Override + public void render(Screen screen, int x, int y, boolean isSelected) { + Font.draw(toString(), screen, x, y, Color.CYAN); + } + }, + + Settings.getEntry("size"), + Settings.getEntry("theme"), + Settings.getEntry("type"), + worldSeed + ) + .setDisplayLength(5) + .setScrollPolicies(0.8f, false) + .setTitle("World Gen Options") + .createMenu() + }; + } + + @Override + public void init(Display parent) { + super.init(parent instanceof TitleDisplay ? parent : new TitleDisplay()); + } +} diff --git a/src/minicraft/screen/WorldSelectDisplay.java b/src/minicraft/screen/WorldSelectDisplay.java new file mode 100644 index 0000000..d6a4c86 --- /dev/null +++ b/src/minicraft/screen/WorldSelectDisplay.java @@ -0,0 +1,202 @@ +package minicraft.screen; + +import java.io.File; +import java.util.ArrayList; + +import minicraft.core.Game; +import minicraft.core.io.InputHandler; +import minicraft.gfx.Color; +import minicraft.gfx.Font; +import minicraft.gfx.Screen; +import minicraft.saveload.Load; +import minicraft.saveload.Save; +import minicraft.saveload.Version; +import minicraft.screen.entry.SelectEntry; + +public class WorldSelectDisplay extends Display { + + // NOTE this will only be responsible for the world load selection screen. + + private static final String worldsDir = Game.gameDir + "/saves/"; + + private static String worldName = ""; + private static boolean loadedWorld = true; + + public static String getWorldName() { return worldName; } + public static void setWorldName(String world, boolean loaded) { + worldName = world; + loadedWorld = loaded; + } + + enum Action { + Copy("C", Color.get(1, 0, 0, 255)), + Rename("R", Color.get(1, 0, 255, 0)), + Delete("D", Color.get(1, 255, 0, 0)); + + public final String key; + public final int color; + + Action(String key, int col) { + this.key = key; + this.color = col; + } + + public static final Action[] values = Action.values(); + } + + private Action curAction = null; + + private static ArrayList worldNames = null; + private static ArrayList worldVersions = new ArrayList<>(); + + public static void refreshWorldNames() { worldNames = null; } + + public static ArrayList getWorldNames() { return getWorldNames(false); } + private static ArrayList getWorldNames(boolean recalc) { + ArrayList worldNames = new ArrayList<>(); + + if(!recalc && WorldSelectDisplay.worldNames != null) { + worldNames.addAll(WorldSelectDisplay.worldNames); + return worldNames; + } + + //find worlds (init step): + File folder = new File(worldsDir); + folder.mkdirs(); + File[] listOfFiles = folder.listFiles(); + + if(listOfFiles == null) { + System.err.println("ERROR: Game location file folder is null, somehow..."); + return new ArrayList<>(); + } + + worldVersions.clear(); + for (int i = 0; i < listOfFiles.length; i++) { + if (listOfFiles[i].isDirectory()) { + String path = worldsDir + listOfFiles[i].getName() + "/"; + File folder2 = new File(path); + folder2.mkdirs(); + String[] files = folder2.list(); + if (files != null && files.length > 0 && files[0].endsWith(Save.extension)) { + String name = listOfFiles[i].getName(); + worldNames.add(name); + if(Game.debug) System.out.println("World found: " + name); + worldVersions.add(new Load(name, false).getWorldVersion()); + } + } + } + + if(WorldSelectDisplay.worldNames == null) + WorldSelectDisplay.worldNames = new ArrayList<>(); + else + WorldSelectDisplay.worldNames.clear(); + + WorldSelectDisplay.worldNames.addAll(worldNames); + + return worldNames; + } + + public WorldSelectDisplay() { + super(true); + } + + @Override + public void init(Display parent) { + super.init(parent instanceof TitleDisplay ? parent : new TitleDisplay()); + worldName = ""; + loadedWorld = true; + + ArrayList worldNames = getWorldNames(true); + + SelectEntry[] entries = new SelectEntry[worldNames.size()]; + + for(int i = 0; i < entries.length; i++) { + String name = worldNames.get(i); + final Version version = worldVersions.get(i); + entries[i] = new SelectEntry(worldNames.get(i), () -> { + if(curAction == null) { + if(version.compareTo(Game.VERSION) > 0) + return; // cannot load a game saved by a higher version! + worldName = name; + Game.setMenu(new LoadingDisplay()); + } + else { + Game.setMenu(new WorldEditDisplay(curAction, name)); + curAction = null; + } + }, false) { + @Override + public int getColor(boolean isSelected) { + if(curAction != null && isSelected) + return curAction.color; + else + return super.getColor(isSelected); + } + }; + } + + + + menus = new Menu[] { + new Menu.Builder(false, 0, RelPos.CENTER, entries) + .setDisplayLength(5) + .setScrollPolicies(1, true) + .createMenu() + }; + } + + public static boolean loadedWorld() { + return loadedWorld; + } + + @Override + public void tick(InputHandler input) { + super.tick(input); + + if(curAction != null) return; + + for(Action a: Action.values) { + if(input.getKey(a.key).clicked) { + curAction = a; + break; + } + } + } + + @Override + public void render(Screen screen) { + super.render(screen); + + int sel = menus[0].getSelection(); + if(sel >= 0 && sel < worldVersions.size()) { + Version version = worldVersions.get(sel); + int col = Color.WHITE; + if(version.compareTo(Game.VERSION) > 0) { + col = Color.RED; + Font.drawCentered("Higher version, cannot load world", screen, Font.textHeight() * 5, col); + } + Font.drawCentered("World Version: " + (version.compareTo(new Version("1.9.2")) <= 0 ? "~" : "") + version, screen, Font.textHeight() * 7/2, col); + } + + Font.drawCentered(Game.input.getMapping("select")+" to confirm", screen, Screen.h - 60, Color.GRAY); + Font.drawCentered(Game.input.getMapping("exit")+" to return", screen, Screen.h - 40, Color.GRAY); + + String title = "Select World"; + int color = Color.WHITE; + + if(curAction == null) { + int y = Screen.h - Font.textHeight() * Action.values.length; + + for (Action a : Action.values) { + Font.drawCentered(a.key + " to " + a, screen, y, a.color); + y += Font.textHeight(); + } + } + else { + title = "Select a World to " + curAction; + color = curAction.color; + } + + Font.drawCentered(title, screen, 0, color); + } +} diff --git a/src/minicraft/screen/entry/ArrayEntry.java b/src/minicraft/screen/entry/ArrayEntry.java new file mode 100644 index 0000000..84ed3ae --- /dev/null +++ b/src/minicraft/screen/entry/ArrayEntry.java @@ -0,0 +1,154 @@ +package minicraft.screen.entry; + +import java.util.Arrays; + +import minicraft.core.io.InputHandler; +import minicraft.core.io.Localization; +import minicraft.core.io.Sound; +import minicraft.gfx.Font; + +public class ArrayEntry extends ListEntry { + + private String label; + private T[] options; + private boolean[] optionVis; + + private int selection; + private boolean wrap; + private boolean localize; + + private int maxWidth; + + private ChangeListener changeAction; + + @SafeVarargs + public ArrayEntry(String label, T... options) { this(label, true, true, options); } + + @SafeVarargs + public ArrayEntry(String label, boolean wrap, T... options) { this(label, wrap, true, options); } + + @SafeVarargs + public ArrayEntry(String label, boolean wrap, boolean localize, T... options) { + this.label = label; + this.options = options; + this.wrap = wrap; + this.localize = localize; + + maxWidth = 0; + for(T option: options) + maxWidth = Math.max(maxWidth, Font.textWidth( + localize ? Localization.getLocalized(option.toString()) : option.toString() + )); + + optionVis = new boolean[options.length]; + Arrays.fill(optionVis, true); + } + + public void setSelection(int idx) { + boolean diff = idx != selection; + if(idx >= 0 && idx < options.length) { + selection = idx; + if(diff && changeAction != null) + changeAction.onChange(getValue()); + } + } + + public void setValue(Object value) { + setSelection(getIndex(value)); // if it is -1, setSelection simply won't set the value. + } + + protected String getLabel() { return label; } + + public int getSelection() { return selection; } + public T getValue() { return options[selection]; } + + public boolean valueIs(Object value) { + if(value instanceof String && options instanceof String[]) + return ((String)value).equalsIgnoreCase((String)getValue()); + else + return getValue().equals(value); + } + + + private int getIndex(Object value) { + boolean areStrings = value instanceof String && options instanceof String[]; + for(int i = 0; i < options.length; i++) { + if(areStrings && ((String)value).equalsIgnoreCase((String)options[i]) || options[i].equals(value)) { + return i; + } + } + + return -1; + } + + + public void setValueVisibility(Object value, boolean visible) { + int idx = getIndex(value); + if(idx >= 0) { + optionVis[idx] = visible; + if(idx == selection && !visible) + moveSelection(1); + } + } + + public boolean getValueVisibility(Object value) { + int idx = getIndex(value); + if(idx < 0) return false; + return optionVis[idx]; + } + + + @Override + public void tick(InputHandler input) { + int prevSel = selection; + int selection = this.selection; + + if(input.getKey("cursor-left").clicked) selection--; + if(input.getKey("cursor-right").clicked) selection++; + + if(prevSel != selection) { + Sound.select.play(); + moveSelection(selection - prevSel); + } + } + + private void moveSelection(int dir) { + // stuff for changing the selection, including skipping locked entries + int prevSel = selection; + int selection = this.selection; + do { + selection += dir; + + if(wrap) { + selection = selection % options.length; + if(selection < 0) selection = options.length - 1; + } else { + selection = Math.min(selection, options.length-1); + selection = Math.max(0, selection); + } + } while(!optionVis[selection] && selection != prevSel); + + setSelection(selection); + } + + @Override + public int getWidth() { + return Font.textWidth(Localization.getLocalized(label)+": ") + maxWidth; + } + + @Override + public String toString() { + String str = Localization.getLocalized(label) + ": "; + String option = options[selection].toString(); + + str += localize ? Localization.getLocalized(option) : option; + + return str; + } + + public void setChangeAction(ChangeListener l) { + this.changeAction = l; + if(l != null) + l.onChange(getValue()); + } +} diff --git a/src/minicraft/screen/entry/BlankEntry.java b/src/minicraft/screen/entry/BlankEntry.java new file mode 100644 index 0000000..5b9c2d3 --- /dev/null +++ b/src/minicraft/screen/entry/BlankEntry.java @@ -0,0 +1,26 @@ +package minicraft.screen.entry; + +import minicraft.core.io.InputHandler; +import minicraft.gfx.Screen; +import minicraft.gfx.SpriteSheet; + +public class BlankEntry extends ListEntry { + + public BlankEntry() { + setSelectable(false); + } + + @Override + public void tick(InputHandler input) {} + + @Override + public void render(Screen screen, int x, int y, boolean isSelected) {} + + @Override + public int getWidth() { + return SpriteSheet.boxWidth; + } + + @Override + public String toString() { return " "; } +} diff --git a/src/minicraft/screen/entry/BooleanEntry.java b/src/minicraft/screen/entry/BooleanEntry.java new file mode 100644 index 0000000..a16c118 --- /dev/null +++ b/src/minicraft/screen/entry/BooleanEntry.java @@ -0,0 +1,20 @@ +package minicraft.screen.entry; + +import minicraft.core.io.Localization; + +public class BooleanEntry extends ArrayEntry { + + public BooleanEntry(String label, boolean initial) { + super(label, true, new Boolean[] {true, false}); + + setSelection(initial ? 0 : 1); + } + + @Override + public String toString() { + return getLabel() + ": " + (getValue() ? + Localization.getLocalized("On") : + Localization.getLocalized("Off") + ); + } +} diff --git a/src/minicraft/screen/entry/ChangeListener.java b/src/minicraft/screen/entry/ChangeListener.java new file mode 100644 index 0000000..4c22ab1 --- /dev/null +++ b/src/minicraft/screen/entry/ChangeListener.java @@ -0,0 +1,6 @@ +package minicraft.screen.entry; + +@FunctionalInterface +public interface ChangeListener { + void onChange(Object newValue); +} diff --git a/src/minicraft/screen/entry/InputEntry.java b/src/minicraft/screen/entry/InputEntry.java new file mode 100644 index 0000000..73e45e7 --- /dev/null +++ b/src/minicraft/screen/entry/InputEntry.java @@ -0,0 +1,61 @@ +package minicraft.screen.entry; + +import minicraft.core.io.InputHandler; +import minicraft.core.io.Localization; +import minicraft.gfx.Color; +import minicraft.gfx.Font; +import minicraft.gfx.Screen; + +public class InputEntry extends ListEntry { + + private String prompt; + private String regex; + private int maxLength; + + private String userInput; + + private ChangeListener listener; + + public InputEntry(String prompt) { + this(prompt, null, 0); + } + public InputEntry(String prompt, String regex, int maxLen) { + this(prompt, regex, maxLen, ""); + } + public InputEntry(String prompt, String regex, int maxLen, String initValue) { + this.prompt = prompt; + this.regex = regex; + this.maxLength = maxLen; + + userInput = initValue; + } + + @Override + public void tick(InputHandler input) { + String prev = userInput; + userInput = input.addKeyTyped(userInput, regex); + if(!prev.equals(userInput) && listener != null) + listener.onChange(userInput); + + if(maxLength > 0 && userInput.length() > maxLength) + userInput = userInput.substring(0, maxLength); // truncates extra + } + + public String getUserInput() { return userInput; } + + public String toString() { + return Localization.getLocalized(prompt)+(prompt.length()==0?"":": ") + userInput; + } + + public void render(Screen screen, int x, int y, boolean isSelected) { + Font.draw(toString(), screen, x, y, isValid() ? isSelected ? Color.GREEN : COL_UNSLCT : Color.RED); + } + + public boolean isValid() { + return userInput.matches(regex); + } + + public void setChangeListener(ChangeListener l) { + listener = l; + } +} diff --git a/src/minicraft/screen/entry/ItemEntry.java b/src/minicraft/screen/entry/ItemEntry.java new file mode 100644 index 0000000..45717c4 --- /dev/null +++ b/src/minicraft/screen/entry/ItemEntry.java @@ -0,0 +1,38 @@ +package minicraft.screen.entry; + +import java.util.List; + +import minicraft.core.io.InputHandler; +import minicraft.gfx.Screen; +import minicraft.item.Item; + +public class ItemEntry extends ListEntry { + + public static ItemEntry[] useItems(List items) { + ItemEntry[] entries = new ItemEntry[items.size()]; + for(int i = 0; i < items.size(); i++) + entries[i] = new ItemEntry(items.get(i)); + return entries; + } + + private Item item; + + public ItemEntry(Item i) { this.item = i; } + + public Item getItem() { return item; } + + @Override + public void tick(InputHandler input) {} + + @Override + public void render(Screen screen, int x, int y, boolean isSelected) { + super.render(screen, x, y, true); + item.sprite.render(screen, x, y); + } + + // if you add to the length of the string, and therefore the width of the entry, then it will actually move the entry RIGHT in the inventory, instead of the intended left, because it is auto-positioned to the left side. + @Override + public String toString() { + return item.getDisplayName(); + } +} diff --git a/src/minicraft/screen/entry/ItemListing.java b/src/minicraft/screen/entry/ItemListing.java new file mode 100644 index 0000000..f7ae3a9 --- /dev/null +++ b/src/minicraft/screen/entry/ItemListing.java @@ -0,0 +1,21 @@ +package minicraft.screen.entry; + +import minicraft.item.Item; + +public class ItemListing extends ItemEntry { + + private String info; + + public ItemListing(Item i, String text) { + super(i); + setSelectable(false); + this.info = text; + } + + public void setText(String text) { info = text; } + + @Override + public String toString() { + return " "+info; + } +} diff --git a/src/minicraft/screen/entry/KeyInputEntry.java b/src/minicraft/screen/entry/KeyInputEntry.java new file mode 100644 index 0000000..097a40d --- /dev/null +++ b/src/minicraft/screen/entry/KeyInputEntry.java @@ -0,0 +1,47 @@ +package minicraft.screen.entry; + +import minicraft.core.io.InputHandler; +import minicraft.core.io.Localization; +import minicraft.gfx.Font; +import minicraft.gfx.Screen; + +public class KeyInputEntry extends SelectEntry { + + private String action, mapping, buffer; + + public KeyInputEntry(String key) { + super("", null); + + this.action = key.substring(0, key.indexOf(";")); + setMapping(key.substring(key.indexOf(";")+1)); + } + + private void setMapping(String mapping) { + this.mapping = mapping; + + StringBuilder buffer = new StringBuilder(); + for(int spaces = 0; spaces < Screen.w/Font.textWidth(" ") - action.length() - mapping.length(); spaces++) + buffer.append(" "); + + this.buffer = buffer.toString(); + } + + @Override + public void tick(InputHandler input) { + if(input.getKey("c").clicked || input.getKey("enter").clicked) + input.changeKeyBinding(action); + else if(input.getKey("a").clicked) + // add a binding, don't remove previous. + input.addKeyBinding(action); + } + + @Override + public int getWidth() { + return Screen.w; + } + + @Override + public String toString() { + return Localization.getLocalized(action) + buffer + mapping; + } +} diff --git a/src/minicraft/screen/entry/LinkEntry.java b/src/minicraft/screen/entry/LinkEntry.java new file mode 100644 index 0000000..93d09f6 --- /dev/null +++ b/src/minicraft/screen/entry/LinkEntry.java @@ -0,0 +1,66 @@ +package minicraft.screen.entry; + +import java.awt.Desktop; +import java.awt.Desktop.Action; +import java.io.IOException; +import java.net.URI; + +import minicraft.core.Game; +import minicraft.core.io.Localization; +import minicraft.screen.BookDisplay; +import minicraft.screen.Menu; +import minicraft.screen.RelPos; +import minicraft.screen.TempDisplay; + +public class LinkEntry extends SelectEntry { + + private static boolean checkedDesktop = false; + private static Desktop desktop = null; + private static boolean canBrowse = false; + + private static final String openMsg = "Opening with browser..."; + + private final int color; + + // note that if the failMsg should be localized, such must be done before passing them as parameters, for this class will not do it since, by default, the failMsg contains a url. + + public LinkEntry(int color, String urlText) { this(color, urlText, urlText, false); } + public LinkEntry(int color, String text, String url) { this(color, text, url, true); } + public LinkEntry(int color, String text, String url, String failMsg) { this(color, text, url, failMsg, true); } + + public LinkEntry(int color, String text, String url, boolean localize) { this(color, text, url, Localization.getLocalized("Go to")+": "+url, localize); } + public LinkEntry(int color, String text, String url, String failMsg, boolean localize) { + super(text, () -> { + if(!checkedDesktop) { + checkedDesktop = true; + if(Desktop.isDesktopSupported()) { + desktop = Desktop.getDesktop(); + canBrowse = desktop.isSupported(Action.BROWSE); + } + } + + if(canBrowse) { + // try to open the download link directly from the browser. + try { + URI uri = URI.create(url); + Game.setMenu(new TempDisplay(3000, false, true, new Menu.Builder(true, 0, RelPos.CENTER, new StringEntry(Localization.getLocalized(openMsg))).createMenu())); + desktop.browse(uri); + } catch(IOException e) { + System.err.println("Could not parse LinkEntry url \""+url+"\" into valid URI:"); + e.printStackTrace(); + canBrowse = false; + } + } + + if(!canBrowse) { + Game.setMenu(new BookDisplay(failMsg, false)); + } + + }, localize); + + this.color = color; + } + + @Override + public int getColor(boolean isSelected) { return color; } +} diff --git a/src/minicraft/screen/entry/ListEntry.java b/src/minicraft/screen/entry/ListEntry.java new file mode 100644 index 0000000..e55c18b --- /dev/null +++ b/src/minicraft/screen/entry/ListEntry.java @@ -0,0 +1,83 @@ +package minicraft.screen.entry; + +import minicraft.core.io.InputHandler; +import minicraft.gfx.Color; +import minicraft.gfx.Font; +import minicraft.gfx.Screen; + +public abstract class ListEntry { + + public static final int COL_UNSLCT = Color.GRAY; + public static final int COL_SLCT = Color.WHITE; + + private boolean selectable = true, visible = true; + + /** + * Ticks the entry. Used to handle input from the InputHandler + * @param input InputHandler used to get player input. + */ + public abstract void tick(InputHandler input); + + /** + * Renders the entry to the given screen. + * Coordinate origin is in the top left corner of the entry space. + * @param screen Screen to render the entry to + * @param x X coordinate + * @param y Y coordinate + * @param isSelected true if the entry is selected, false otherwise + */ + public void render(Screen screen, int x, int y, boolean isSelected) { + if(visible) + Font.draw(toString(), screen, x, y, getColor(isSelected)); + } + + /** + * Returns the current color depending on if the entry is selected. + * @param isSelected true if the entry is selected, false otherwise + * @return the current entry color + */ + public int getColor(boolean isSelected) { return isSelected ? COL_SLCT : COL_UNSLCT; } + + /** + * Calculates the width of the entry. + * @return the entry's width + */ + public int getWidth() { + return Font.textWidth(toString()); + } + + /** + * Calculates the height of the entry. + * @return the entry's height + */ + public static int getHeight() { + return Font.textHeight(); + } + + /** + * Determines if this entry can be selected. + * @return true if it is visible and can be selected, false otherwise. + */ + public final boolean isSelectable() { return selectable && visible; } + + /** + * Returns whether the entry is visible or not. + * @return true if the entry is visible, false otherwise + */ + public final boolean isVisible() { return visible; } + + /** + * Changes if the entry can be selected or not. + * @param selectable true if the entry can be selected, false if not + */ + public final void setSelectable(boolean selectable) { this.selectable = selectable; } + + /** + * Changes if the entry is visible or not. + * @param visible true if the entry should be visible, false if not + */ + public final void setVisible(boolean visible) { this.visible = visible; } + + @Override + public abstract String toString(); +} diff --git a/src/minicraft/screen/entry/RangeEntry.java b/src/minicraft/screen/entry/RangeEntry.java new file mode 100644 index 0000000..a46244a --- /dev/null +++ b/src/minicraft/screen/entry/RangeEntry.java @@ -0,0 +1,31 @@ +package minicraft.screen.entry; + +public class RangeEntry extends ArrayEntry { + + private static Integer[] getIntegerArray(int min, int max) { + Integer[] ints = new Integer[max-min+1]; + + for(int i = 0; i < ints.length; i++) + ints[i] = min+i; + + return ints; + } + + private int min, max; + + public RangeEntry(String label, int min, int max, int initial) { + super(label, false, getIntegerArray(min, max)); + + this.min = min; + this.max = max; + + setValue(initial); + } + + @Override + public void setValue(Object o) { + if(!(o instanceof Integer)) return; + + setSelection(((Integer)o)-min); + } +} diff --git a/src/minicraft/screen/entry/RecipeEntry.java b/src/minicraft/screen/entry/RecipeEntry.java new file mode 100644 index 0000000..4d74285 --- /dev/null +++ b/src/minicraft/screen/entry/RecipeEntry.java @@ -0,0 +1,41 @@ +package minicraft.screen.entry; + +import java.util.List; + +import minicraft.core.io.InputHandler; +import minicraft.gfx.Font; +import minicraft.gfx.Screen; +import minicraft.item.Recipe; + +public class RecipeEntry extends ItemEntry { + + public static RecipeEntry[] useRecipes(List recipes) { + RecipeEntry[] entries = new RecipeEntry[recipes.size()]; + for(int i = 0; i < recipes.size(); i++) + entries[i] = new RecipeEntry(recipes.get(i)); + return entries; + } + + private Recipe recipe; + + public RecipeEntry(Recipe r) { + super(r.getProduct()); + this.recipe = r; + } + + @Override + public void tick(InputHandler input) {} + + @Override + public void render(Screen screen, int x, int y, boolean isSelected) { + if(isVisible()) { + Font.draw(toString(), screen, x, y, recipe.getCanCraft() ? COL_SLCT : COL_UNSLCT); + getItem().sprite.render(screen, x, y); + } + } + + @Override + public String toString() { + return super.toString() + (recipe.getAmount() > 1 ? " x" + recipe.getAmount() : ""); + } +} diff --git a/src/minicraft/screen/entry/SelectEntry.java b/src/minicraft/screen/entry/SelectEntry.java new file mode 100644 index 0000000..fbdf6a8 --- /dev/null +++ b/src/minicraft/screen/entry/SelectEntry.java @@ -0,0 +1,47 @@ +package minicraft.screen.entry; + +import minicraft.core.Action; +import minicraft.core.io.InputHandler; +import minicraft.core.io.Localization; +import minicraft.core.io.Sound; +import minicraft.gfx.Font; + +public class SelectEntry extends ListEntry { + + private Action onSelect; + private String text; + private boolean localize; + + /** + * Creates a new entry which acts as a button. + * Can do an action when it is selected. + * @param text Text displayed on this entry + * @param onSelect Action which happens when the entry is selected + */ + public SelectEntry(String text, Action onSelect) { this(text, onSelect, true); } + public SelectEntry(String text, Action onSelect, boolean localize) { + this.onSelect = onSelect; + this.text = text; + this.localize = localize; + } + + /** + * Changes the text of the entry. + * @param text new text + */ + void setText(String text) { this.text = text; } + + @Override + public void tick(InputHandler input) { + if(input.getKey("select").clicked && onSelect != null) { + Sound.confirm.play(); + onSelect.act(); + } + } + + @Override + public int getWidth() { return Font.textWidth(toString()); } + + @Override + public String toString() { return localize ? Localization.getLocalized(text) : text; } +} diff --git a/src/minicraft/screen/entry/StringEntry.java b/src/minicraft/screen/entry/StringEntry.java new file mode 100644 index 0000000..1e80300 --- /dev/null +++ b/src/minicraft/screen/entry/StringEntry.java @@ -0,0 +1,45 @@ +package minicraft.screen.entry; + +import minicraft.core.io.InputHandler; +import minicraft.gfx.Color; + +// an unselectable line. +public class StringEntry extends ListEntry { + + private static final int DEFAULT_COLOR = Color.WHITE; + + private String text; + private int color; + + /** + * + */ + public static StringEntry[] useLines(String... lines) { + return useLines(DEFAULT_COLOR, lines); + } + public static StringEntry[] useLines(int color, String... lines) { + StringEntry[] entries = new StringEntry[lines.length]; + for(int i = 0; i < lines.length; i++) + entries[i] = new StringEntry(lines[i], color); + + return entries; + } + + public StringEntry(String text) { + this(text, DEFAULT_COLOR); + } + public StringEntry(String text, int color) { + setSelectable(false); + this.text = text; + this.color = color; + } + + @Override + public void tick(InputHandler input) {} + + @Override + public int getColor(boolean isSelected) { return color; } + + @Override + public String toString() { return text; } +}