1. Welcome to the Brawl website! Feel free to look around our forums. Join our growing community by typing /register in-game!

Technology WarZ Mechanics

Discussion in 'Off Topic' started by BeastyBoo, Feb 11, 2021.

Thread Status:
Please be aware that this thread is more than 30 days old. Do not post unless the topic can still be discussed. Read more...
  1. BeastyBoo

    BeastyBoo Insert custom title here

    Joined:
    May 10, 2015
    Messages:
    347
    Ratings:
    +21
    Hey, I currently have way to much spare time on my hands due to COVID-19, and therefore decided to make some possible WarZ Mechanics for those who wonder how it really works. This should not break any guidelines, as I won't be hard-coding WarZ mechanics, but rather similar objects, in order to gain a better understanding of the game-mechanics.

    I won't be describing in details everything I do, as I expect the people reading to have some form of knowledge with Java.

    First, I wanted to introduce the simplest feature, and that is basic food (Beans, pasta etc...)

    Since I wont be hard-coding each food, I decided to create an object instead, and try to follow the known adapter structural design pattern, which relies on connecting classes thru interfaces. If I was going to hard-code it, I would rather utilize inheritance thru a decorator design pattern.

    First, the simple food object:
    Code:
    private final Material material;
        private final double health;
        private final int delay;
    
        public Food(Material material, double health, int delay) {
            this.material = material;
            this.health = health;
            this.delay = delay;
        }
    
        public Material getMaterial() {
            return material;
        }
    
        public double getHealth() {
            return health;
        }
    
        public int getDelay() {
            return delay;
        }
    
        @Override
        public boolean equals(Object o) {
            if (this == o) return true;
            if (o == null || getClass() != o.getClass()) return false;
            Food food = (Food) o;
            return Double.compare(food.health, health) == 0 && delay == food.delay && material == food.material;
        }
    
        @Override
        public int hashCode() {
            return Objects.hash(material, health, delay);
        }
    
        @Override
        public String toString() {
            return "Food{" +
                    "material=" + material +
                    ", health=" + health +
                    ", delay=" + delay +
                    '}';
        }

    Then the interface:
    Code:
    public interface FoodRepository {
    
        void load();
    
        void close();
    
        boolean createFood(Player player, double health, int delay);
    
        boolean deleteFood(Player player);
    
        boolean useFood(Player player);
    
        Food getFood(Material material);
    
        Map<Material, Food> getFoods();
    
    }

    And then the service, or some might call it the manager class where we actually give our methods meaning.
    Code:
    public class FoodService implements FoodRepository {
    
        private final WarZ core;
        private final Map<Material, Food> foodMap;
        private final Map<UUID, Long> delayMap;
    
        public FoodService(WarZ core) {
            this.core = core;
            foodMap = new HashMap<>();
            delayMap = new HashMap<>();
        }
    
        @Override
        public void load() {
            //Load cache
        }
    
        @Override
        public void close() {
            //Save cache
        }
    
        @Override
        public boolean createFood(Player player, double health, int delay) {
            if(health <= 0) {
                return false;
            }
    
            Material material = player.getInventory().getItemInMainHand().getType();
    
            if(this.getFood(material) != null) {
                return false;
            }
    
            Food food = new Food(material, health, delay);
            foodMap.put(material, food);
            return true;
        }
    
        @Override
        public boolean deleteFood(Player player) {
            Material material = player.getInventory().getItemInMainHand().getType();
            Food food = this.getFood(material);
    
            if(food == null) {
                return false;
            }
    
            foodMap.remove(material, food);
            return true;
        }
    
        @Override
        public boolean useFood(Player player) {
            ItemStack itemInMainHand = player.getInventory().getItemInMainHand();
            Food food = this.getFood(itemInMainHand.getType());
    
            if(food == null) {
                return false;
            }
    
            if(delayMap.containsKey(player.getUniqueId())) {
                if(System.currentTimeMillis() < delayMap.get(player.getUniqueId())) {
                    return false;
                }
                delayMap.remove(player.getUniqueId());
            }
    
            double health = player.getHealth();
            player.setHealth(health + food.getHealth());
    
            long delay = System.currentTimeMillis() + (food.getDelay() * 1000L);
            delayMap.put(player.getUniqueId(), delay);
    
            int amount = itemInMainHand.getAmount();
            if(amount > 1) {
                itemInMainHand.setAmount(amount - 1);
            } else {
                player.getInventory().remove(itemInMainHand);
            }
            return true;
        }
    
        @Override
        public Food getFood(Material material) {
            return foodMap.get(material);
        }
    
        @Override
        public Map<Material, Food> getFoods() {
            return foodMap;
        }
    }

    As you may have noticed, I've not bothered to save the cached food objects, as that is not related to the mechanics I'm targetting.

    To make this work, we need to setup a class which holds our repository interface, and use these methods inside an event, such as below:
    Code:
    private final WarZ core;
    
        public FoodListener(WarZ core) {
            this.core = core;
        }
    
        @EventHandler
        public void useFood(PlayerInteractEvent event) {
            if(event.getAction() == Action.RIGHT_CLICK_AIR || event.getAction() == Action.RIGHT_CLICK_BLOCK) {
    
                if(!event.getHand().equals(EquipmentSlot.HAND)) {
                    return;
                }
    
                core.getAPI().useFood(event.getPlayer());
            }
        }
    EDIT: I noticed I used .equals instead of ==. Since this is an enum value, therefore we want to check the refrence (memory location) instead.

    Hope someone enjoyed this, and ask questions if there is something you don't understand.

    The entire project is up on github: https://github.com/BeastyBoo/WarZ-Mechanics/tree/master/src/main/java/com/github/beastyboo/warz

    For those a bit curious about Java programming; since Java is an object-orientated programming language, it's considered better practice to decouple your "Main" class from you're core class, which is why I have a empty WarZPlugin class, which just initialize the core class itself.

    Also, I decided to avoid the singleton pattern as much as possible, as they conceal hidden dependencies, and make testing much harder. Instead I decided to use a crude form of dependency injection, where I just pass down the core class instance to whatever class needs it, limiting the global access of it.

    I also chose to have all the "Main" classes inside the same package, in order to utilize the protected access modifier, to make sure outside plugins can't load/unload the core features, and harm the plugin.

    Since we are storing the objects in a map, we need to implement hashCode and equals, as we normally should for safety. And I also vastly prefer my objects immutable when possible, which we could on this occasion.
     
    #1 BeastyBoo, Feb 11, 2021
    Last edited: Feb 13, 2021
  2. RyGuy

    RyGuy developer man

    Joined:
    Aug 8, 2013
    Messages:
    1,958
    Ratings:
    +1,062
    only thing you’re missing is support for an item stacks data (ie how basically every food is the same material but diff dye colors) and i wouldn’t have stored it like that but looks pretty good tbh
     
  3. BeastyBoo

    BeastyBoo Insert custom title here

    Joined:
    May 10, 2015
    Messages:
    347
    Ratings:
    +21
    Yeah, I could of used ItemStack instead of material, but for the sake of simplicity of the mechanics I decided to avoid supporting it. For those interested it could easily be changed, as well as making it more advanced to support more advanced objects such as Sugar, or gapples etc..

    If so, I would not store it the same way, and rather use the decorator design pattern as I stated above.

    May I ask how you would store the cached objects that is more efficient than the simple map?
     
  4. TTIP

    TTIP Former WW & Warz Mod

    Joined:
    Dec 10, 2017
    Messages:
    8
    Ratings:
    +12
    BeastyBoo for warz dev :smile:
     
  5. BeastyBoo

    BeastyBoo Insert custom title here

    Joined:
    May 10, 2015
    Messages:
    347
    Ratings:
    +21
    LOOT-TABLES:

    A custom loot-table feature created from scratch.

    Loot-Table object:
    Code:
    public class LootTable {
    
        private final String name;
        private final Set<LootTableItem> lootTableItems;
    
        public LootTable(String name, Set<LootTableItem> lootTableItems) {
            this.name = name;
            this.lootTableItems = lootTableItems;
        }
    
        public String getName() {
            return name;
        }
    
        public Set<LootTableItem> getLootTableItems() {
            return lootTableItems;
        }
    
        @Override
        public boolean equals(Object o) {
            if (this == o) return true;
            if (o == null || getClass() != o.getClass()) return false;
            LootTable lootTable = (LootTable) o;
            return name.equals(lootTable.name) && lootTableItems.equals(lootTable.lootTableItems);
        }
    
        @Override
        public int hashCode() {
            return Objects.hash(name, lootTableItems);
        }
    
        @Override
        public String toString() {
            return "LootTable{" +
                    "name='" + name + '\'' +
                    ", lootTableItems=" + lootTableItems +
                    '}';
        }
    }
    EDIT: The reason why I store the LootTableItems in a Set, rather in a map, is because I'm never getting the Items thru a key in my methods, since the core feature here is to add a random item. (It will always scale linear in my methods anyway)

    Loot-Table item object:

    Code:
    public class LootTableItem {
    
        private final ItemStack itemStack;
        private final double percentage;
    
        public LootTableItem(ItemStack itemStack, double percentage) {
            this.itemStack = itemStack;
            this.percentage = percentage;
        }
    
        public ItemStack getItemStack() {
            return itemStack;
        }
    
        public double getPercentage() {
            return percentage;
        }
    
        @Override
        public boolean equals(Object o) {
            if (this == o) return true;
            if (o == null || getClass() != o.getClass()) return false;
            LootTableItem that = (LootTableItem) o;
            return Double.compare(that.percentage, percentage) == 0 && itemStack.equals(that.itemStack);
        }
    
        @Override
        public int hashCode() {
            return Objects.hash(itemStack, percentage);
        }
    
        @Override
        public String toString() {
            return "LootTableItem{" +
                    "itemStack=" + itemStack +
                    ", percentage=" + percentage +
                    '}';
        }
    }

    How you want to handle them may differ from each individual. Personally, I will create a sort of Manager which controls and contain each loot-table. Note; Using this, you can create as many loot-tables as you want, which means they are not dependent of a zone. What I would do is create a Zone object which contains the reference of a certain selected loot-table using a hashtable. Preferably chest-location as key, and loot-table as value.

    The reason I would save the location of each chest inside the zone, connected with a loot-table is how easily you can add/remove bugged chests, including the time complexity would change tremendous. When adding items inside the chests, the method will scale linear to the amount of objects inside the hashtable. However, when adding/removing a chest it will always be constant, compared to if we did not use a map. Iterating over each block on the entire map would not be ideal due to the amount of blocks, however it will also have a linear time complexity.


    When adding items into the chests, you'd want to loop over each zone, and then loop over each key (chest-location). <- Quadratic, but rather unavoidable when avoiding iterating over large chucks.

    Example of how to add items:
    Code:
    for(Location location : arena.getLootTableMap().keySet()) {
                LootTable lootTable = arena.getLootTableMap().get(location);
    
                if(location.getBlock().getType() == Material.CHEST) {
                    Chest chest = (Chest) location.getBlock().getState();
                    chest.getInventory().clear();
                    Random random = new Random();
    
                    for(int i = 0; i<2; i++) {
                        int inv = random.nextInt((chest.getInventory().getSize() ) ;
                        chest.getInventory().setItem(inv, getRandomItemStack(lootTable.getLootTableItems()));
                    }
                }
            }

    Getting a random LootTableItem from the Hashset
    Code:
    private ItemStack getRandomItemStack(Set<LootTableItem> items) {
            TreeMap<Double, LootTableItem> map = new TreeMap<>();
            double total = 0.0d;
    
            for(LootTableItem item : items) {
                double chance = item.getPercentage()/100;
                map.put(total += chance, item);
            }
    
            Random random = new Random();
            double value = random.nextDouble();
    
            LootTableItem object = map.ceilingEntry(value).getValue();
    
            return object.getItemStack();
        }

    Note: this method uses ceilingEntry(), meaning it will get the key at a higher value than our inserted value. If the combined percentages don't add up too 100, (or 1 depending on how you set it up), it can produce an NPE if there is no key registered at a higher value than our random value.

    My LootTable "manager":
    Code:
    public class LootTableService implements LootTableRepository {
    
        private final WarZ core;
        private final Map<String, LootTable> lootTableMap;
    
        public LootTableService(WarZ core) {
            this.core = core;
            this.lootTableMap = new HashMap<>();
        }
    
        @Override
        public boolean createLootTable(Player player, String name) {
    
            if(this.getLootTable(name) != null) {
                player.sendMessage("Loot table already exist");
                return false;
            }
    
            LootTable lootTable = new LootTable(name, new HashSet<>());
            lootTableMap.put(name.toLowerCase(), lootTable);
            player.sendMessage("Created loot table: " + name);
            return true;
        }
    
        @Override
        public boolean deleteLootTable(Player player, String name) {
            LootTable lootTable = this.getLootTable(name);
    
            if(lootTable == null) {
                player.sendMessage("Loot table don't exist");
                return false;
            }
    
            lootTableMap.remove(name.toLowerCase(), lootTable);
            player.sendMessage("Deleted loot table: " + lootTable.getName());
            lootTable = null;
    
            return true;
        }
    
        @Override
        public boolean addLootTableItem(Player player, String name, int amount, double percentage) {
            LootTable lootTable = this.getLootTable(name);
    
            if(lootTable == null) {
                player.sendMessage("Loot table don't exist");
                return false;
            }
    
            if(player.getInventory().getItemInMainHand().getType() == Material.AIR) {
                player.sendMessage("You can't add AIR into the loot-table");
                return false;
            }
    
            ItemStack itemStack = new ItemStack(player.getInventory().getItemInMainHand());
    
            itemStack.setAmount(amount);
            LootTableItem lootTableItem = new LootTableItem(itemStack, percentage);
    
            if(lootTable.getLootTableItems().contains(lootTableItem)) {
                player.sendMessage("Loot table item is already registered");
                return false;
            }
    
            lootTable.getLootTableItems().add(lootTableItem);
    
            player.sendMessage("Loot table item added!");
            return true;
        }
    
        @Override
        public boolean openLootTableGUI(Player player, String name) {
            LootTable lootTable = this.getLootTable(name);
    
            if(lootTable == null) {
                player.sendMessage("Loot table don't exist");
                return false;
            }
    
            LootTableGUI.Factory.openInventory(player, lootTable);
            return true;
        }
    
        @Override
        public boolean deleteLootTableItem(Player player, LootTable lootTable, LootTableItem lootTableItem) {
    
            lootTable.getLootTableItems().remove(lootTableItem);
            player.sendMessage("Removed loot table item!");
            return true;
        }
    
        @Override
        public boolean displayLootTableList(Player player) {
    
            player.sendMessage("§cLoot-Tables: ");
            for(LootTable table : lootTableMap.values()) {
                player.sendMessage("§6 - " + table.getName());
            }
    
            return true;
        }
    
        @Override
        public boolean addLootItemsToChest(LootTable lootTable, Inventory inventory) {
            for(LootTableItem item : lootTable.getLootTableItems()) {
                inventory.addItem(item.getItemStack());
            }
            return true;
        }
    
        @Override
        public LootTable getLootTable(String name) {
            return lootTableMap.get(name.toLowerCase());
        }
    
        @Override
        public Map<String, LootTable> getLootTableMap() {
            return lootTableMap;
        }
    
    }
     
    #5 BeastyBoo, Feb 22, 2021
    Last edited: Feb 25, 2021
Loading...
Similar Threads Forum Date
Idea WARZ UPDATE TWEAK by PumkinLp MC-WarZ Apr 9, 2021
Warz 20 is fun MC-WarZ Mar 7, 2021
[MC-WARZ] Tutorial EP #3 - Looting, Travel and teams (With Staff Alting) MC-WarZ Feb 22, 2021
Remove 21 and add solo option for warz MC-WarZ Jan 21, 2021
WarZ YouTube Tutorials Guides Jan 8, 2021
Thread Status:
Please be aware that this thread is more than 30 days old. Do not post unless the topic can still be discussed. Read more...