diff --git a/L2J_Mobius_01.0_Ertheia/dist/game/data/scripts/handlers/bypasshandlers/NpcViewMod.java b/L2J_Mobius_01.0_Ertheia/dist/game/data/scripts/handlers/bypasshandlers/NpcViewMod.java index 35d31315ad..fab79abca8 100644 --- a/L2J_Mobius_01.0_Ertheia/dist/game/data/scripts/handlers/bypasshandlers/NpcViewMod.java +++ b/L2J_Mobius_01.0_Ertheia/dist/game/data/scripts/handlers/bypasshandlers/NpcViewMod.java @@ -37,6 +37,7 @@ import org.l2jmobius.gameserver.model.actor.Attackable; import org.l2jmobius.gameserver.model.actor.Creature; import org.l2jmobius.gameserver.model.actor.Npc; import org.l2jmobius.gameserver.model.actor.Player; +import org.l2jmobius.gameserver.model.holders.DropGroupHolder; import org.l2jmobius.gameserver.model.holders.DropHolder; import org.l2jmobius.gameserver.model.item.ItemTemplate; import org.l2jmobius.gameserver.model.itemcontainer.Inventory; @@ -326,12 +327,13 @@ public class NpcViewMod implements IBypassHandler private static String getDropListButtons(Npc npc) { final StringBuilder sb = new StringBuilder(); + final List dropListGroups = npc.getTemplate().getDropGroups(); final List dropListDeath = npc.getTemplate().getDropList(); final List dropListSpoil = npc.getTemplate().getSpoilList(); - if ((dropListDeath != null) || (dropListSpoil != null)) + if ((dropListGroups != null) || (dropListDeath != null) || (dropListSpoil != null)) { sb.append(""); - if (dropListDeath != null) + if ((dropListGroups != null) || (dropListDeath != null)) { sb.append(""); } @@ -348,13 +350,40 @@ public class NpcViewMod implements IBypassHandler private void sendNpcDropList(Player player, Npc npc, DropType dropType, int pageValue) { - final List templateList = dropType == DropType.SPOIL ? npc.getTemplate().getSpoilList() : npc.getTemplate().getDropList(); - if (templateList == null) + List dropList = null; + if (dropType == DropType.SPOIL) + { + dropList = new ArrayList<>(npc.getTemplate().getSpoilList()); + } + else + { + final List drops = npc.getTemplate().getDropList(); + if (drops != null) + { + dropList = new ArrayList<>(drops); + } + final List dropGroups = npc.getTemplate().getDropGroups(); + if (dropGroups != null) + { + if (dropList == null) + { + dropList = new ArrayList<>(); + } + for (DropGroupHolder dropGroup : dropGroups) + { + final double chance = dropGroup.getChance() / 100; + for (DropHolder dropHolder : dropGroup.getDropList()) + { + dropList.add(new DropHolder(dropHolder.getDropType(), dropHolder.getItemId(), dropHolder.getMin(), dropHolder.getMax(), dropHolder.getChance() * chance)); + } + } + } + } + if (dropList == null) { return; } - final List dropList = new ArrayList<>(templateList); Collections.sort(dropList, (d1, d2) -> Integer.valueOf(d1.getItemId()).compareTo(Integer.valueOf(d2.getItemId()))); int pages = dropList.size() / DROP_LIST_ITEMS_PER_PAGE; diff --git a/L2J_Mobius_01.0_Ertheia/dist/game/data/scripts/handlers/communityboard/DropSearchBoard.java b/L2J_Mobius_01.0_Ertheia/dist/game/data/scripts/handlers/communityboard/DropSearchBoard.java index 647d14cec5..be1d97660b 100644 --- a/L2J_Mobius_01.0_Ertheia/dist/game/data/scripts/handlers/communityboard/DropSearchBoard.java +++ b/L2J_Mobius_01.0_Ertheia/dist/game/data/scripts/handlers/communityboard/DropSearchBoard.java @@ -36,6 +36,7 @@ import org.l2jmobius.gameserver.handler.CommunityBoardHandler; import org.l2jmobius.gameserver.handler.IParseBoardHandler; import org.l2jmobius.gameserver.model.actor.Player; import org.l2jmobius.gameserver.model.actor.templates.NpcTemplate; +import org.l2jmobius.gameserver.model.holders.DropGroupHolder; import org.l2jmobius.gameserver.model.holders.DropHolder; import org.l2jmobius.gameserver.model.item.ItemTemplate; import org.l2jmobius.gameserver.model.itemcontainer.Inventory; @@ -104,6 +105,17 @@ public class DropSearchBoard implements IParseBoardHandler private void buildDropIndex() { + NpcData.getInstance().getTemplates(npc -> npc.getDropGroups() != null).forEach(npcTemplate -> + { + for (DropGroupHolder dropGroup : npcTemplate.getDropGroups()) + { + final double chance = dropGroup.getChance() / 100; + for (DropHolder dropHolder : dropGroup.getDropList()) + { + addToDropList(npcTemplate, new DropHolder(dropHolder.getDropType(), dropHolder.getItemId(), dropHolder.getMin(), dropHolder.getMax(), dropHolder.getChance() * chance)); + } + } + }); NpcData.getInstance().getTemplates(npc -> npc.getDropList() != null).forEach(npcTemplate -> { for (DropHolder dropHolder : npcTemplate.getDropList()) diff --git a/L2J_Mobius_01.0_Ertheia/dist/game/data/xsd/npcs.xsd b/L2J_Mobius_01.0_Ertheia/dist/game/data/xsd/npcs.xsd index 117ca4cc1d..2c6e583383 100644 --- a/L2J_Mobius_01.0_Ertheia/dist/game/data/xsd/npcs.xsd +++ b/L2J_Mobius_01.0_Ertheia/dist/game/data/xsd/npcs.xsd @@ -1,16 +1,5 @@ - - - - - - - - - - - @@ -260,11 +249,81 @@ - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/L2J_Mobius_01.0_Ertheia/java/org/l2jmobius/gameserver/data/xml/NpcData.java b/L2J_Mobius_01.0_Ertheia/java/org/l2jmobius/gameserver/data/xml/NpcData.java index 28850ef6bb..d6d5210168 100644 --- a/L2J_Mobius_01.0_Ertheia/java/org/l2jmobius/gameserver/data/xml/NpcData.java +++ b/L2J_Mobius_01.0_Ertheia/java/org/l2jmobius/gameserver/data/xml/NpcData.java @@ -46,6 +46,7 @@ import org.l2jmobius.gameserver.enums.MpRewardType; import org.l2jmobius.gameserver.model.StatSet; import org.l2jmobius.gameserver.model.actor.templates.NpcTemplate; import org.l2jmobius.gameserver.model.effects.EffectType; +import org.l2jmobius.gameserver.model.holders.DropGroupHolder; import org.l2jmobius.gameserver.model.holders.DropHolder; import org.l2jmobius.gameserver.model.skill.Skill; @@ -100,6 +101,7 @@ public class NpcData implements IXmlReader Set clans = null; Set ignoreClanNpcIds = null; List dropLists = null; + List dropGroups = null; set.set("id", npcId); set.set("displayId", parseInteger(attrs, "displayId")); set.set("level", parseByte(attrs, "level")); @@ -436,24 +438,54 @@ public class NpcData implements IXmlReader if (dropType != null) { - if (dropLists == null) - { - dropLists = new ArrayList<>(); - } - for (Node dropNode = dropListsNode.getFirstChild(); dropNode != null; dropNode = dropNode.getNextSibling()) { - final NamedNodeMap dropAttrs = dropNode.getAttributes(); - if ("item".equalsIgnoreCase(dropNode.getNodeName())) + final String nodeName = dropNode.getNodeName(); + if (nodeName.equalsIgnoreCase("group")) { - final DropHolder dropItem = new DropHolder(dropType, parseInteger(dropAttrs, "id"), parseLong(dropAttrs, "min"), parseLong(dropAttrs, "max"), parseDouble(dropAttrs, "chance")); - if (ItemTable.getInstance().getTemplate(parseInteger(dropAttrs, "id")) == null) + if (dropGroups == null) { - LOGGER.warning("DropListItem: Could not find item with id " + parseInteger(dropAttrs, "id") + "."); + dropGroups = new ArrayList<>(); + } + + final DropGroupHolder group = new DropGroupHolder(parseDouble(dropNode.getAttributes(), "chance")); + for (Node groupNode = dropNode.getFirstChild(); groupNode != null; groupNode = groupNode.getNextSibling()) + { + if (groupNode.getNodeName().equalsIgnoreCase("item")) + { + final NamedNodeMap groupAttrs = groupNode.getAttributes(); + final int itemId = parseInteger(groupAttrs, "id"); + + if (ItemTable.getInstance().getTemplate(itemId) == null) + { + LOGGER.warning("DropListItem: Could not find item with id " + itemId + "."); + } + else + { + group.addDrop(new DropHolder(dropType, itemId, parseLong(groupAttrs, "min"), parseLong(groupAttrs, "max"), parseDouble(groupAttrs, "chance"))); + } + } + } + + dropGroups.add(group); + } + else if (nodeName.equalsIgnoreCase("item")) + { + if (dropLists == null) + { + dropLists = new ArrayList<>(); + } + + final NamedNodeMap dropAttrs = dropNode.getAttributes(); + final int itemId = parseInteger(dropAttrs, "id"); + + if (ItemTable.getInstance().getTemplate(itemId) == null) + { + LOGGER.warning("DropListItem: Could not find item with id " + itemId + "."); } else { - dropLists.add(dropItem); + dropLists.add(new DropHolder(dropType, itemId, parseLong(dropAttrs, "min"), parseLong(dropAttrs, "max"), parseDouble(dropAttrs, "chance"))); } } } @@ -612,10 +644,21 @@ public class NpcData implements IXmlReader template.setClans(clans); template.setIgnoreClanNpcIds(ignoreClanNpcIds); + // Clean old drop groups. + template.removeDropGroups(); + + // Set new drop groups. + if (dropGroups != null) + { + template.setDropGroups(dropGroups); + } + + // Clean old drop lists. + template.removeDrops(); + + // Set new drop lists. if (dropLists != null) { - template.removeDrops(); - // Drops are sorted by chance (high to low). Collections.sort(dropLists, (d1, d2) -> Double.valueOf(d2.getChance()).compareTo(Double.valueOf(d1.getChance()))); for (DropHolder dropHolder : dropLists) diff --git a/L2J_Mobius_01.0_Ertheia/java/org/l2jmobius/gameserver/model/actor/templates/NpcTemplate.java b/L2J_Mobius_01.0_Ertheia/java/org/l2jmobius/gameserver/model/actor/templates/NpcTemplate.java index 3e40f76887..0c282ce418 100644 --- a/L2J_Mobius_01.0_Ertheia/java/org/l2jmobius/gameserver/model/actor/templates/NpcTemplate.java +++ b/L2J_Mobius_01.0_Ertheia/java/org/l2jmobius/gameserver/model/actor/templates/NpcTemplate.java @@ -36,6 +36,7 @@ import org.l2jmobius.gameserver.enums.Race; import org.l2jmobius.gameserver.enums.Sex; import org.l2jmobius.gameserver.model.StatSet; import org.l2jmobius.gameserver.model.actor.Creature; +import org.l2jmobius.gameserver.model.holders.DropGroupHolder; import org.l2jmobius.gameserver.model.holders.DropHolder; import org.l2jmobius.gameserver.model.holders.ItemHolder; import org.l2jmobius.gameserver.model.interfaces.IIdentifiable; @@ -107,6 +108,7 @@ public class NpcTemplate extends CreatureTemplate implements IIdentifiable private Map> _aiSkillLists; private Set _clans; private Set _ignoreClanNpcIds; + private List _dropGroups; private List _dropListDeath; private List _dropListSpoil; private float _collisionRadiusGrown; @@ -649,12 +651,22 @@ public class NpcTemplate extends CreatureTemplate implements IIdentifiable _ignoreClanNpcIds = ignoreClanNpcIds != null ? Collections.unmodifiableSet(ignoreClanNpcIds) : null; } + public void removeDropGroups() + { + _dropGroups = null; + } + public void removeDrops() { _dropListDeath = null; _dropListSpoil = null; } + public void setDropGroups(List groups) + { + _dropGroups = groups; + } + public void addDrop(DropHolder dropHolder) { if (_dropListDeath == null) @@ -673,6 +685,11 @@ public class NpcTemplate extends CreatureTemplate implements IIdentifiable _dropListSpoil.add(dropHolder); } + public List getDropGroups() + { + return _dropGroups; + } + public List getDropList() { return _dropListDeath; @@ -685,11 +702,201 @@ public class NpcTemplate extends CreatureTemplate implements IIdentifiable public List calculateDrops(DropType dropType, Creature victim, Creature killer) { - final List dropList = dropType == DropType.SPOIL ? _dropListSpoil : _dropListDeath; - if (dropList == null) + if (dropType == DropType.DROP) { - return null; + // calculate group drops + List groupDrops = null; + if (_dropGroups != null) + { + groupDrops = calculateGroupDrops(victim, killer); + } + + // calculate ungrouped drops + List ungroupedDrops = null; + if (_dropListDeath != null) + { + ungroupedDrops = calculateUngroupedDrops(dropType, victim, killer); + } + + // return results + if ((groupDrops != null) && (ungroupedDrops != null)) + { + groupDrops.addAll(ungroupedDrops); + ungroupedDrops.clear(); + return groupDrops; + } + if (groupDrops != null) + { + return groupDrops; + } + if (ungroupedDrops != null) + { + return ungroupedDrops; + } } + else if ((dropType == DropType.SPOIL) && (_dropListSpoil != null)) + { + return calculateUngroupedDrops(dropType, victim, killer); + } + + // no drops + return null; + } + + private List calculateGroupDrops(Creature victim, Creature killer) + { + // level difference calculations + final int levelDifference = victim.getLevel() - killer.getLevel(); + final double levelGapChanceToDropAdena = Util.map(levelDifference, -Config.DROP_ADENA_MAX_LEVEL_DIFFERENCE, -Config.DROP_ADENA_MIN_LEVEL_DIFFERENCE, Config.DROP_ADENA_MIN_LEVEL_GAP_CHANCE, 100d); + final double levelGapChanceToDrop = Util.map(levelDifference, -Config.DROP_ITEM_MAX_LEVEL_DIFFERENCE, -Config.DROP_ITEM_MIN_LEVEL_DIFFERENCE, Config.DROP_ITEM_MIN_LEVEL_GAP_CHANCE, 100d); + + int dropOccurrenceCounter = victim.isRaid() ? Config.DROP_MAX_OCCURRENCES_RAIDBOSS : Config.DROP_MAX_OCCURRENCES_NORMAL; + List calculatedDrops = null; + for (DropGroupHolder group : _dropGroups) + { + if (dropOccurrenceCounter <= 0) + { + break; + } + + double groupRate = 1; + final List groupDrops = group.getDropList(); + for (DropHolder dropItem : groupDrops) + { + final int itemId = dropItem.getItemId(); + final ItemTemplate item = ItemTable.getInstance().getTemplate(itemId); + final boolean champion = victim.isChampion(); + + // chance + double rateChance = 1; + final Float itemChance = Config.RATE_DROP_CHANCE_BY_ID.get(itemId); + if (itemChance != null) + { + if (itemChance == 0) + { + continue; + } + + rateChance *= itemChance; + if (champion && (itemId == Inventory.ADENA_ID)) + { + rateChance *= Config.CHAMPION_ADENAS_REWARDS_CHANCE; + } + } + else if (item.hasExImmediateEffect()) + { + rateChance *= Config.RATE_HERB_DROP_CHANCE_MULTIPLIER; + } + else if (victim.isRaid()) + { + rateChance *= Config.RATE_RAID_DROP_CHANCE_MULTIPLIER; + } + else + { + rateChance *= Config.RATE_DEATH_DROP_CHANCE_MULTIPLIER * (champion ? Config.CHAMPION_REWARDS_CHANCE : 1); + } + + // premium chance + if (Config.PREMIUM_SYSTEM_ENABLED && (killer.getActingPlayer() != null) && killer.getActingPlayer().hasPremiumStatus()) + { + if (Config.PREMIUM_RATE_DROP_CHANCE_BY_ID.get(itemId) != null) + { + rateChance *= Config.PREMIUM_RATE_DROP_CHANCE_BY_ID.get(itemId); + } + else if (item.hasExImmediateEffect()) + { + // TODO: Premium herb chance? :) + } + else if (victim.isRaid()) + { + // TODO: Premium raid chance? :) + } + else + { + rateChance *= Config.PREMIUM_RATE_DROP_CHANCE; + } + } + + // keep lowest to avoid chance by id configuration conflicts + groupRate = Math.min(groupRate, rateChance); + } + + if ((Rnd.nextDouble() * 100) < (group.getChance() * groupRate)) + { + double totalChance = 0; // total group chance is 100 + final double dropChance = Rnd.nextDouble() * 100; + GROUP_DROP: for (DropHolder dropItem : groupDrops) + { + if (dropOccurrenceCounter <= 0) + { + break; + } + + // calculate if item will drop + totalChance += dropItem.getChance(); + if (dropChance >= totalChance) + { + continue; + } + + // check level gap that may prevent to drop item + if ((Rnd.nextDouble() * 100) > (dropItem.getItemId() == Inventory.ADENA_ID ? levelGapChanceToDropAdena : levelGapChanceToDrop)) + { + continue; + } + + // create the drop + final ItemHolder drop = calculateGroupDrop(dropItem, victim, killer); + + // create list + if (calculatedDrops == null) + { + calculatedDrops = new ArrayList<>(dropOccurrenceCounter); + } + + // finally + if (group.getChance() < 100) + { + dropOccurrenceCounter--; + } + calculatedDrops.add(drop); + + // no more drops from this group + break GROUP_DROP; + } + } + } + + // champion extra drop + if (victim.isChampion()) + { + if ((victim.getLevel() < killer.getLevel()) && (Rnd.get(100) < Config.CHAMPION_REWARD_LOWER_LEVEL_ITEM_CHANCE)) + { + return calculatedDrops; + } + if ((victim.getLevel() > killer.getLevel()) && (Rnd.get(100) < Config.CHAMPION_REWARD_HIGHER_LEVEL_ITEM_CHANCE)) + { + return calculatedDrops; + } + + // create list + if (calculatedDrops == null) + { + calculatedDrops = new ArrayList<>(); + } + + if (!calculatedDrops.containsAll(Config.CHAMPION_REWARD_ITEMS)) + { + calculatedDrops.addAll(Config.CHAMPION_REWARD_ITEMS); + } + } + + return calculatedDrops; + } + + private List calculateUngroupedDrops(DropType dropType, Creature victim, Creature killer) + { + final List dropList = dropType == DropType.SPOIL ? _dropListSpoil : _dropListDeath; // level difference calculations final int levelDifference = victim.getLevel() - killer.getLevel(); @@ -721,7 +928,7 @@ public class NpcTemplate extends CreatureTemplate implements IIdentifiable } // calculate chances - final ItemHolder drop = calculateDrop(dropItem, victim, killer); + final ItemHolder drop = calculateUngroupedDrop(dropItem, victim, killer); if (drop == null) { continue; @@ -776,20 +983,82 @@ public class NpcTemplate extends CreatureTemplate implements IIdentifiable calculatedDrops = new ArrayList<>(); } - calculatedDrops.addAll(Config.CHAMPION_REWARD_ITEMS); + if (!calculatedDrops.containsAll(Config.CHAMPION_REWARD_ITEMS)) + { + calculatedDrops.addAll(Config.CHAMPION_REWARD_ITEMS); + } } return calculatedDrops; } /** - * All item drop chance calculations are done by this method. * @param dropItem * @param victim * @param killer * @return ItemHolder */ - private ItemHolder calculateDrop(DropHolder dropItem, Creature victim, Creature killer) + private ItemHolder calculateGroupDrop(DropHolder dropItem, Creature victim, Creature killer) + { + final int itemId = dropItem.getItemId(); + final ItemTemplate item = ItemTable.getInstance().getTemplate(itemId); + final boolean champion = victim.isChampion(); + + // calculate amount + double rateAmount = 1; + if (Config.RATE_DROP_AMOUNT_BY_ID.get(itemId) != null) + { + rateAmount *= Config.RATE_DROP_AMOUNT_BY_ID.get(itemId); + if (champion && (itemId == Inventory.ADENA_ID)) + { + rateAmount *= Config.CHAMPION_ADENAS_REWARDS_AMOUNT; + } + } + else if (item.hasExImmediateEffect()) + { + rateAmount *= Config.RATE_HERB_DROP_AMOUNT_MULTIPLIER; + } + else if (victim.isRaid()) + { + rateAmount *= Config.RATE_RAID_DROP_AMOUNT_MULTIPLIER; + } + else + { + rateAmount *= Config.RATE_DEATH_DROP_AMOUNT_MULTIPLIER * (champion ? Config.CHAMPION_REWARDS_AMOUNT : 1); + } + + // premium amount + if (Config.PREMIUM_SYSTEM_ENABLED && (killer.getActingPlayer() != null) && killer.getActingPlayer().hasPremiumStatus()) + { + if (Config.PREMIUM_RATE_DROP_AMOUNT_BY_ID.get(itemId) != null) + { + rateAmount *= Config.PREMIUM_RATE_DROP_AMOUNT_BY_ID.get(itemId); + } + else if (item.hasExImmediateEffect()) + { + // TODO: Premium herb amount? :) + } + else if (victim.isRaid()) + { + // TODO: Premium raid amount? :) + } + else + { + rateAmount *= Config.PREMIUM_RATE_DROP_AMOUNT; + } + } + + // finally + return new ItemHolder(itemId, (long) (Rnd.get(dropItem.getMin(), dropItem.getMax()) * rateAmount)); + } + + /** + * @param dropItem + * @param victim + * @param killer + * @return ItemHolder + */ + private ItemHolder calculateUngroupedDrop(DropHolder dropItem, Creature victim, Creature killer) { switch (dropItem.getDropType()) { diff --git a/L2J_Mobius_01.0_Ertheia/java/org/l2jmobius/gameserver/model/holders/DropGroupHolder.java b/L2J_Mobius_01.0_Ertheia/java/org/l2jmobius/gameserver/model/holders/DropGroupHolder.java new file mode 100644 index 0000000000..6c2401c0b0 --- /dev/null +++ b/L2J_Mobius_01.0_Ertheia/java/org/l2jmobius/gameserver/model/holders/DropGroupHolder.java @@ -0,0 +1,49 @@ +/* + * This file is part of the L2J Mobius project. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package org.l2jmobius.gameserver.model.holders; + +import java.util.ArrayList; +import java.util.List; + +/** + * @author Mobius + */ +public class DropGroupHolder +{ + private final List _dropList = new ArrayList<>(); + private final double _chance; + + public DropGroupHolder(double chance) + { + _chance = chance; + } + + public List getDropList() + { + return _dropList; + } + + public void addDrop(DropHolder holder) + { + _dropList.add(holder); + } + + public double getChance() + { + return _chance; + } +} diff --git a/L2J_Mobius_02.5_Underground/dist/game/data/scripts/handlers/bypasshandlers/NpcViewMod.java b/L2J_Mobius_02.5_Underground/dist/game/data/scripts/handlers/bypasshandlers/NpcViewMod.java index 35d31315ad..fab79abca8 100644 --- a/L2J_Mobius_02.5_Underground/dist/game/data/scripts/handlers/bypasshandlers/NpcViewMod.java +++ b/L2J_Mobius_02.5_Underground/dist/game/data/scripts/handlers/bypasshandlers/NpcViewMod.java @@ -37,6 +37,7 @@ import org.l2jmobius.gameserver.model.actor.Attackable; import org.l2jmobius.gameserver.model.actor.Creature; import org.l2jmobius.gameserver.model.actor.Npc; import org.l2jmobius.gameserver.model.actor.Player; +import org.l2jmobius.gameserver.model.holders.DropGroupHolder; import org.l2jmobius.gameserver.model.holders.DropHolder; import org.l2jmobius.gameserver.model.item.ItemTemplate; import org.l2jmobius.gameserver.model.itemcontainer.Inventory; @@ -326,12 +327,13 @@ public class NpcViewMod implements IBypassHandler private static String getDropListButtons(Npc npc) { final StringBuilder sb = new StringBuilder(); + final List dropListGroups = npc.getTemplate().getDropGroups(); final List dropListDeath = npc.getTemplate().getDropList(); final List dropListSpoil = npc.getTemplate().getSpoilList(); - if ((dropListDeath != null) || (dropListSpoil != null)) + if ((dropListGroups != null) || (dropListDeath != null) || (dropListSpoil != null)) { sb.append("
"); - if (dropListDeath != null) + if ((dropListGroups != null) || (dropListDeath != null)) { sb.append(""); } @@ -348,13 +350,40 @@ public class NpcViewMod implements IBypassHandler private void sendNpcDropList(Player player, Npc npc, DropType dropType, int pageValue) { - final List templateList = dropType == DropType.SPOIL ? npc.getTemplate().getSpoilList() : npc.getTemplate().getDropList(); - if (templateList == null) + List dropList = null; + if (dropType == DropType.SPOIL) + { + dropList = new ArrayList<>(npc.getTemplate().getSpoilList()); + } + else + { + final List drops = npc.getTemplate().getDropList(); + if (drops != null) + { + dropList = new ArrayList<>(drops); + } + final List dropGroups = npc.getTemplate().getDropGroups(); + if (dropGroups != null) + { + if (dropList == null) + { + dropList = new ArrayList<>(); + } + for (DropGroupHolder dropGroup : dropGroups) + { + final double chance = dropGroup.getChance() / 100; + for (DropHolder dropHolder : dropGroup.getDropList()) + { + dropList.add(new DropHolder(dropHolder.getDropType(), dropHolder.getItemId(), dropHolder.getMin(), dropHolder.getMax(), dropHolder.getChance() * chance)); + } + } + } + } + if (dropList == null) { return; } - final List dropList = new ArrayList<>(templateList); Collections.sort(dropList, (d1, d2) -> Integer.valueOf(d1.getItemId()).compareTo(Integer.valueOf(d2.getItemId()))); int pages = dropList.size() / DROP_LIST_ITEMS_PER_PAGE; diff --git a/L2J_Mobius_02.5_Underground/dist/game/data/scripts/handlers/communityboard/DropSearchBoard.java b/L2J_Mobius_02.5_Underground/dist/game/data/scripts/handlers/communityboard/DropSearchBoard.java index 647d14cec5..be1d97660b 100644 --- a/L2J_Mobius_02.5_Underground/dist/game/data/scripts/handlers/communityboard/DropSearchBoard.java +++ b/L2J_Mobius_02.5_Underground/dist/game/data/scripts/handlers/communityboard/DropSearchBoard.java @@ -36,6 +36,7 @@ import org.l2jmobius.gameserver.handler.CommunityBoardHandler; import org.l2jmobius.gameserver.handler.IParseBoardHandler; import org.l2jmobius.gameserver.model.actor.Player; import org.l2jmobius.gameserver.model.actor.templates.NpcTemplate; +import org.l2jmobius.gameserver.model.holders.DropGroupHolder; import org.l2jmobius.gameserver.model.holders.DropHolder; import org.l2jmobius.gameserver.model.item.ItemTemplate; import org.l2jmobius.gameserver.model.itemcontainer.Inventory; @@ -104,6 +105,17 @@ public class DropSearchBoard implements IParseBoardHandler private void buildDropIndex() { + NpcData.getInstance().getTemplates(npc -> npc.getDropGroups() != null).forEach(npcTemplate -> + { + for (DropGroupHolder dropGroup : npcTemplate.getDropGroups()) + { + final double chance = dropGroup.getChance() / 100; + for (DropHolder dropHolder : dropGroup.getDropList()) + { + addToDropList(npcTemplate, new DropHolder(dropHolder.getDropType(), dropHolder.getItemId(), dropHolder.getMin(), dropHolder.getMax(), dropHolder.getChance() * chance)); + } + } + }); NpcData.getInstance().getTemplates(npc -> npc.getDropList() != null).forEach(npcTemplate -> { for (DropHolder dropHolder : npcTemplate.getDropList()) diff --git a/L2J_Mobius_02.5_Underground/dist/game/data/xsd/npcs.xsd b/L2J_Mobius_02.5_Underground/dist/game/data/xsd/npcs.xsd index 117ca4cc1d..2c6e583383 100644 --- a/L2J_Mobius_02.5_Underground/dist/game/data/xsd/npcs.xsd +++ b/L2J_Mobius_02.5_Underground/dist/game/data/xsd/npcs.xsd @@ -1,16 +1,5 @@ - - - - - - - - - - - @@ -260,11 +249,81 @@ - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/L2J_Mobius_02.5_Underground/java/org/l2jmobius/gameserver/data/xml/NpcData.java b/L2J_Mobius_02.5_Underground/java/org/l2jmobius/gameserver/data/xml/NpcData.java index 28850ef6bb..d6d5210168 100644 --- a/L2J_Mobius_02.5_Underground/java/org/l2jmobius/gameserver/data/xml/NpcData.java +++ b/L2J_Mobius_02.5_Underground/java/org/l2jmobius/gameserver/data/xml/NpcData.java @@ -46,6 +46,7 @@ import org.l2jmobius.gameserver.enums.MpRewardType; import org.l2jmobius.gameserver.model.StatSet; import org.l2jmobius.gameserver.model.actor.templates.NpcTemplate; import org.l2jmobius.gameserver.model.effects.EffectType; +import org.l2jmobius.gameserver.model.holders.DropGroupHolder; import org.l2jmobius.gameserver.model.holders.DropHolder; import org.l2jmobius.gameserver.model.skill.Skill; @@ -100,6 +101,7 @@ public class NpcData implements IXmlReader Set clans = null; Set ignoreClanNpcIds = null; List dropLists = null; + List dropGroups = null; set.set("id", npcId); set.set("displayId", parseInteger(attrs, "displayId")); set.set("level", parseByte(attrs, "level")); @@ -436,24 +438,54 @@ public class NpcData implements IXmlReader if (dropType != null) { - if (dropLists == null) - { - dropLists = new ArrayList<>(); - } - for (Node dropNode = dropListsNode.getFirstChild(); dropNode != null; dropNode = dropNode.getNextSibling()) { - final NamedNodeMap dropAttrs = dropNode.getAttributes(); - if ("item".equalsIgnoreCase(dropNode.getNodeName())) + final String nodeName = dropNode.getNodeName(); + if (nodeName.equalsIgnoreCase("group")) { - final DropHolder dropItem = new DropHolder(dropType, parseInteger(dropAttrs, "id"), parseLong(dropAttrs, "min"), parseLong(dropAttrs, "max"), parseDouble(dropAttrs, "chance")); - if (ItemTable.getInstance().getTemplate(parseInteger(dropAttrs, "id")) == null) + if (dropGroups == null) { - LOGGER.warning("DropListItem: Could not find item with id " + parseInteger(dropAttrs, "id") + "."); + dropGroups = new ArrayList<>(); + } + + final DropGroupHolder group = new DropGroupHolder(parseDouble(dropNode.getAttributes(), "chance")); + for (Node groupNode = dropNode.getFirstChild(); groupNode != null; groupNode = groupNode.getNextSibling()) + { + if (groupNode.getNodeName().equalsIgnoreCase("item")) + { + final NamedNodeMap groupAttrs = groupNode.getAttributes(); + final int itemId = parseInteger(groupAttrs, "id"); + + if (ItemTable.getInstance().getTemplate(itemId) == null) + { + LOGGER.warning("DropListItem: Could not find item with id " + itemId + "."); + } + else + { + group.addDrop(new DropHolder(dropType, itemId, parseLong(groupAttrs, "min"), parseLong(groupAttrs, "max"), parseDouble(groupAttrs, "chance"))); + } + } + } + + dropGroups.add(group); + } + else if (nodeName.equalsIgnoreCase("item")) + { + if (dropLists == null) + { + dropLists = new ArrayList<>(); + } + + final NamedNodeMap dropAttrs = dropNode.getAttributes(); + final int itemId = parseInteger(dropAttrs, "id"); + + if (ItemTable.getInstance().getTemplate(itemId) == null) + { + LOGGER.warning("DropListItem: Could not find item with id " + itemId + "."); } else { - dropLists.add(dropItem); + dropLists.add(new DropHolder(dropType, itemId, parseLong(dropAttrs, "min"), parseLong(dropAttrs, "max"), parseDouble(dropAttrs, "chance"))); } } } @@ -612,10 +644,21 @@ public class NpcData implements IXmlReader template.setClans(clans); template.setIgnoreClanNpcIds(ignoreClanNpcIds); + // Clean old drop groups. + template.removeDropGroups(); + + // Set new drop groups. + if (dropGroups != null) + { + template.setDropGroups(dropGroups); + } + + // Clean old drop lists. + template.removeDrops(); + + // Set new drop lists. if (dropLists != null) { - template.removeDrops(); - // Drops are sorted by chance (high to low). Collections.sort(dropLists, (d1, d2) -> Double.valueOf(d2.getChance()).compareTo(Double.valueOf(d1.getChance()))); for (DropHolder dropHolder : dropLists) diff --git a/L2J_Mobius_02.5_Underground/java/org/l2jmobius/gameserver/model/actor/templates/NpcTemplate.java b/L2J_Mobius_02.5_Underground/java/org/l2jmobius/gameserver/model/actor/templates/NpcTemplate.java index 3e40f76887..0c282ce418 100644 --- a/L2J_Mobius_02.5_Underground/java/org/l2jmobius/gameserver/model/actor/templates/NpcTemplate.java +++ b/L2J_Mobius_02.5_Underground/java/org/l2jmobius/gameserver/model/actor/templates/NpcTemplate.java @@ -36,6 +36,7 @@ import org.l2jmobius.gameserver.enums.Race; import org.l2jmobius.gameserver.enums.Sex; import org.l2jmobius.gameserver.model.StatSet; import org.l2jmobius.gameserver.model.actor.Creature; +import org.l2jmobius.gameserver.model.holders.DropGroupHolder; import org.l2jmobius.gameserver.model.holders.DropHolder; import org.l2jmobius.gameserver.model.holders.ItemHolder; import org.l2jmobius.gameserver.model.interfaces.IIdentifiable; @@ -107,6 +108,7 @@ public class NpcTemplate extends CreatureTemplate implements IIdentifiable private Map> _aiSkillLists; private Set _clans; private Set _ignoreClanNpcIds; + private List _dropGroups; private List _dropListDeath; private List _dropListSpoil; private float _collisionRadiusGrown; @@ -649,12 +651,22 @@ public class NpcTemplate extends CreatureTemplate implements IIdentifiable _ignoreClanNpcIds = ignoreClanNpcIds != null ? Collections.unmodifiableSet(ignoreClanNpcIds) : null; } + public void removeDropGroups() + { + _dropGroups = null; + } + public void removeDrops() { _dropListDeath = null; _dropListSpoil = null; } + public void setDropGroups(List groups) + { + _dropGroups = groups; + } + public void addDrop(DropHolder dropHolder) { if (_dropListDeath == null) @@ -673,6 +685,11 @@ public class NpcTemplate extends CreatureTemplate implements IIdentifiable _dropListSpoil.add(dropHolder); } + public List getDropGroups() + { + return _dropGroups; + } + public List getDropList() { return _dropListDeath; @@ -685,11 +702,201 @@ public class NpcTemplate extends CreatureTemplate implements IIdentifiable public List calculateDrops(DropType dropType, Creature victim, Creature killer) { - final List dropList = dropType == DropType.SPOIL ? _dropListSpoil : _dropListDeath; - if (dropList == null) + if (dropType == DropType.DROP) { - return null; + // calculate group drops + List groupDrops = null; + if (_dropGroups != null) + { + groupDrops = calculateGroupDrops(victim, killer); + } + + // calculate ungrouped drops + List ungroupedDrops = null; + if (_dropListDeath != null) + { + ungroupedDrops = calculateUngroupedDrops(dropType, victim, killer); + } + + // return results + if ((groupDrops != null) && (ungroupedDrops != null)) + { + groupDrops.addAll(ungroupedDrops); + ungroupedDrops.clear(); + return groupDrops; + } + if (groupDrops != null) + { + return groupDrops; + } + if (ungroupedDrops != null) + { + return ungroupedDrops; + } } + else if ((dropType == DropType.SPOIL) && (_dropListSpoil != null)) + { + return calculateUngroupedDrops(dropType, victim, killer); + } + + // no drops + return null; + } + + private List calculateGroupDrops(Creature victim, Creature killer) + { + // level difference calculations + final int levelDifference = victim.getLevel() - killer.getLevel(); + final double levelGapChanceToDropAdena = Util.map(levelDifference, -Config.DROP_ADENA_MAX_LEVEL_DIFFERENCE, -Config.DROP_ADENA_MIN_LEVEL_DIFFERENCE, Config.DROP_ADENA_MIN_LEVEL_GAP_CHANCE, 100d); + final double levelGapChanceToDrop = Util.map(levelDifference, -Config.DROP_ITEM_MAX_LEVEL_DIFFERENCE, -Config.DROP_ITEM_MIN_LEVEL_DIFFERENCE, Config.DROP_ITEM_MIN_LEVEL_GAP_CHANCE, 100d); + + int dropOccurrenceCounter = victim.isRaid() ? Config.DROP_MAX_OCCURRENCES_RAIDBOSS : Config.DROP_MAX_OCCURRENCES_NORMAL; + List calculatedDrops = null; + for (DropGroupHolder group : _dropGroups) + { + if (dropOccurrenceCounter <= 0) + { + break; + } + + double groupRate = 1; + final List groupDrops = group.getDropList(); + for (DropHolder dropItem : groupDrops) + { + final int itemId = dropItem.getItemId(); + final ItemTemplate item = ItemTable.getInstance().getTemplate(itemId); + final boolean champion = victim.isChampion(); + + // chance + double rateChance = 1; + final Float itemChance = Config.RATE_DROP_CHANCE_BY_ID.get(itemId); + if (itemChance != null) + { + if (itemChance == 0) + { + continue; + } + + rateChance *= itemChance; + if (champion && (itemId == Inventory.ADENA_ID)) + { + rateChance *= Config.CHAMPION_ADENAS_REWARDS_CHANCE; + } + } + else if (item.hasExImmediateEffect()) + { + rateChance *= Config.RATE_HERB_DROP_CHANCE_MULTIPLIER; + } + else if (victim.isRaid()) + { + rateChance *= Config.RATE_RAID_DROP_CHANCE_MULTIPLIER; + } + else + { + rateChance *= Config.RATE_DEATH_DROP_CHANCE_MULTIPLIER * (champion ? Config.CHAMPION_REWARDS_CHANCE : 1); + } + + // premium chance + if (Config.PREMIUM_SYSTEM_ENABLED && (killer.getActingPlayer() != null) && killer.getActingPlayer().hasPremiumStatus()) + { + if (Config.PREMIUM_RATE_DROP_CHANCE_BY_ID.get(itemId) != null) + { + rateChance *= Config.PREMIUM_RATE_DROP_CHANCE_BY_ID.get(itemId); + } + else if (item.hasExImmediateEffect()) + { + // TODO: Premium herb chance? :) + } + else if (victim.isRaid()) + { + // TODO: Premium raid chance? :) + } + else + { + rateChance *= Config.PREMIUM_RATE_DROP_CHANCE; + } + } + + // keep lowest to avoid chance by id configuration conflicts + groupRate = Math.min(groupRate, rateChance); + } + + if ((Rnd.nextDouble() * 100) < (group.getChance() * groupRate)) + { + double totalChance = 0; // total group chance is 100 + final double dropChance = Rnd.nextDouble() * 100; + GROUP_DROP: for (DropHolder dropItem : groupDrops) + { + if (dropOccurrenceCounter <= 0) + { + break; + } + + // calculate if item will drop + totalChance += dropItem.getChance(); + if (dropChance >= totalChance) + { + continue; + } + + // check level gap that may prevent to drop item + if ((Rnd.nextDouble() * 100) > (dropItem.getItemId() == Inventory.ADENA_ID ? levelGapChanceToDropAdena : levelGapChanceToDrop)) + { + continue; + } + + // create the drop + final ItemHolder drop = calculateGroupDrop(dropItem, victim, killer); + + // create list + if (calculatedDrops == null) + { + calculatedDrops = new ArrayList<>(dropOccurrenceCounter); + } + + // finally + if (group.getChance() < 100) + { + dropOccurrenceCounter--; + } + calculatedDrops.add(drop); + + // no more drops from this group + break GROUP_DROP; + } + } + } + + // champion extra drop + if (victim.isChampion()) + { + if ((victim.getLevel() < killer.getLevel()) && (Rnd.get(100) < Config.CHAMPION_REWARD_LOWER_LEVEL_ITEM_CHANCE)) + { + return calculatedDrops; + } + if ((victim.getLevel() > killer.getLevel()) && (Rnd.get(100) < Config.CHAMPION_REWARD_HIGHER_LEVEL_ITEM_CHANCE)) + { + return calculatedDrops; + } + + // create list + if (calculatedDrops == null) + { + calculatedDrops = new ArrayList<>(); + } + + if (!calculatedDrops.containsAll(Config.CHAMPION_REWARD_ITEMS)) + { + calculatedDrops.addAll(Config.CHAMPION_REWARD_ITEMS); + } + } + + return calculatedDrops; + } + + private List calculateUngroupedDrops(DropType dropType, Creature victim, Creature killer) + { + final List dropList = dropType == DropType.SPOIL ? _dropListSpoil : _dropListDeath; // level difference calculations final int levelDifference = victim.getLevel() - killer.getLevel(); @@ -721,7 +928,7 @@ public class NpcTemplate extends CreatureTemplate implements IIdentifiable } // calculate chances - final ItemHolder drop = calculateDrop(dropItem, victim, killer); + final ItemHolder drop = calculateUngroupedDrop(dropItem, victim, killer); if (drop == null) { continue; @@ -776,20 +983,82 @@ public class NpcTemplate extends CreatureTemplate implements IIdentifiable calculatedDrops = new ArrayList<>(); } - calculatedDrops.addAll(Config.CHAMPION_REWARD_ITEMS); + if (!calculatedDrops.containsAll(Config.CHAMPION_REWARD_ITEMS)) + { + calculatedDrops.addAll(Config.CHAMPION_REWARD_ITEMS); + } } return calculatedDrops; } /** - * All item drop chance calculations are done by this method. * @param dropItem * @param victim * @param killer * @return ItemHolder */ - private ItemHolder calculateDrop(DropHolder dropItem, Creature victim, Creature killer) + private ItemHolder calculateGroupDrop(DropHolder dropItem, Creature victim, Creature killer) + { + final int itemId = dropItem.getItemId(); + final ItemTemplate item = ItemTable.getInstance().getTemplate(itemId); + final boolean champion = victim.isChampion(); + + // calculate amount + double rateAmount = 1; + if (Config.RATE_DROP_AMOUNT_BY_ID.get(itemId) != null) + { + rateAmount *= Config.RATE_DROP_AMOUNT_BY_ID.get(itemId); + if (champion && (itemId == Inventory.ADENA_ID)) + { + rateAmount *= Config.CHAMPION_ADENAS_REWARDS_AMOUNT; + } + } + else if (item.hasExImmediateEffect()) + { + rateAmount *= Config.RATE_HERB_DROP_AMOUNT_MULTIPLIER; + } + else if (victim.isRaid()) + { + rateAmount *= Config.RATE_RAID_DROP_AMOUNT_MULTIPLIER; + } + else + { + rateAmount *= Config.RATE_DEATH_DROP_AMOUNT_MULTIPLIER * (champion ? Config.CHAMPION_REWARDS_AMOUNT : 1); + } + + // premium amount + if (Config.PREMIUM_SYSTEM_ENABLED && (killer.getActingPlayer() != null) && killer.getActingPlayer().hasPremiumStatus()) + { + if (Config.PREMIUM_RATE_DROP_AMOUNT_BY_ID.get(itemId) != null) + { + rateAmount *= Config.PREMIUM_RATE_DROP_AMOUNT_BY_ID.get(itemId); + } + else if (item.hasExImmediateEffect()) + { + // TODO: Premium herb amount? :) + } + else if (victim.isRaid()) + { + // TODO: Premium raid amount? :) + } + else + { + rateAmount *= Config.PREMIUM_RATE_DROP_AMOUNT; + } + } + + // finally + return new ItemHolder(itemId, (long) (Rnd.get(dropItem.getMin(), dropItem.getMax()) * rateAmount)); + } + + /** + * @param dropItem + * @param victim + * @param killer + * @return ItemHolder + */ + private ItemHolder calculateUngroupedDrop(DropHolder dropItem, Creature victim, Creature killer) { switch (dropItem.getDropType()) { diff --git a/L2J_Mobius_02.5_Underground/java/org/l2jmobius/gameserver/model/holders/DropGroupHolder.java b/L2J_Mobius_02.5_Underground/java/org/l2jmobius/gameserver/model/holders/DropGroupHolder.java new file mode 100644 index 0000000000..6c2401c0b0 --- /dev/null +++ b/L2J_Mobius_02.5_Underground/java/org/l2jmobius/gameserver/model/holders/DropGroupHolder.java @@ -0,0 +1,49 @@ +/* + * This file is part of the L2J Mobius project. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package org.l2jmobius.gameserver.model.holders; + +import java.util.ArrayList; +import java.util.List; + +/** + * @author Mobius + */ +public class DropGroupHolder +{ + private final List _dropList = new ArrayList<>(); + private final double _chance; + + public DropGroupHolder(double chance) + { + _chance = chance; + } + + public List getDropList() + { + return _dropList; + } + + public void addDrop(DropHolder holder) + { + _dropList.add(holder); + } + + public double getChance() + { + return _chance; + } +} diff --git a/L2J_Mobius_03.0_Helios/dist/game/data/scripts/handlers/bypasshandlers/NpcViewMod.java b/L2J_Mobius_03.0_Helios/dist/game/data/scripts/handlers/bypasshandlers/NpcViewMod.java index 35d31315ad..fab79abca8 100644 --- a/L2J_Mobius_03.0_Helios/dist/game/data/scripts/handlers/bypasshandlers/NpcViewMod.java +++ b/L2J_Mobius_03.0_Helios/dist/game/data/scripts/handlers/bypasshandlers/NpcViewMod.java @@ -37,6 +37,7 @@ import org.l2jmobius.gameserver.model.actor.Attackable; import org.l2jmobius.gameserver.model.actor.Creature; import org.l2jmobius.gameserver.model.actor.Npc; import org.l2jmobius.gameserver.model.actor.Player; +import org.l2jmobius.gameserver.model.holders.DropGroupHolder; import org.l2jmobius.gameserver.model.holders.DropHolder; import org.l2jmobius.gameserver.model.item.ItemTemplate; import org.l2jmobius.gameserver.model.itemcontainer.Inventory; @@ -326,12 +327,13 @@ public class NpcViewMod implements IBypassHandler private static String getDropListButtons(Npc npc) { final StringBuilder sb = new StringBuilder(); + final List dropListGroups = npc.getTemplate().getDropGroups(); final List dropListDeath = npc.getTemplate().getDropList(); final List dropListSpoil = npc.getTemplate().getSpoilList(); - if ((dropListDeath != null) || (dropListSpoil != null)) + if ((dropListGroups != null) || (dropListDeath != null) || (dropListSpoil != null)) { sb.append("
"); - if (dropListDeath != null) + if ((dropListGroups != null) || (dropListDeath != null)) { sb.append(""); } @@ -348,13 +350,40 @@ public class NpcViewMod implements IBypassHandler private void sendNpcDropList(Player player, Npc npc, DropType dropType, int pageValue) { - final List templateList = dropType == DropType.SPOIL ? npc.getTemplate().getSpoilList() : npc.getTemplate().getDropList(); - if (templateList == null) + List dropList = null; + if (dropType == DropType.SPOIL) + { + dropList = new ArrayList<>(npc.getTemplate().getSpoilList()); + } + else + { + final List drops = npc.getTemplate().getDropList(); + if (drops != null) + { + dropList = new ArrayList<>(drops); + } + final List dropGroups = npc.getTemplate().getDropGroups(); + if (dropGroups != null) + { + if (dropList == null) + { + dropList = new ArrayList<>(); + } + for (DropGroupHolder dropGroup : dropGroups) + { + final double chance = dropGroup.getChance() / 100; + for (DropHolder dropHolder : dropGroup.getDropList()) + { + dropList.add(new DropHolder(dropHolder.getDropType(), dropHolder.getItemId(), dropHolder.getMin(), dropHolder.getMax(), dropHolder.getChance() * chance)); + } + } + } + } + if (dropList == null) { return; } - final List dropList = new ArrayList<>(templateList); Collections.sort(dropList, (d1, d2) -> Integer.valueOf(d1.getItemId()).compareTo(Integer.valueOf(d2.getItemId()))); int pages = dropList.size() / DROP_LIST_ITEMS_PER_PAGE; diff --git a/L2J_Mobius_03.0_Helios/dist/game/data/scripts/handlers/communityboard/DropSearchBoard.java b/L2J_Mobius_03.0_Helios/dist/game/data/scripts/handlers/communityboard/DropSearchBoard.java index 647d14cec5..be1d97660b 100644 --- a/L2J_Mobius_03.0_Helios/dist/game/data/scripts/handlers/communityboard/DropSearchBoard.java +++ b/L2J_Mobius_03.0_Helios/dist/game/data/scripts/handlers/communityboard/DropSearchBoard.java @@ -36,6 +36,7 @@ import org.l2jmobius.gameserver.handler.CommunityBoardHandler; import org.l2jmobius.gameserver.handler.IParseBoardHandler; import org.l2jmobius.gameserver.model.actor.Player; import org.l2jmobius.gameserver.model.actor.templates.NpcTemplate; +import org.l2jmobius.gameserver.model.holders.DropGroupHolder; import org.l2jmobius.gameserver.model.holders.DropHolder; import org.l2jmobius.gameserver.model.item.ItemTemplate; import org.l2jmobius.gameserver.model.itemcontainer.Inventory; @@ -104,6 +105,17 @@ public class DropSearchBoard implements IParseBoardHandler private void buildDropIndex() { + NpcData.getInstance().getTemplates(npc -> npc.getDropGroups() != null).forEach(npcTemplate -> + { + for (DropGroupHolder dropGroup : npcTemplate.getDropGroups()) + { + final double chance = dropGroup.getChance() / 100; + for (DropHolder dropHolder : dropGroup.getDropList()) + { + addToDropList(npcTemplate, new DropHolder(dropHolder.getDropType(), dropHolder.getItemId(), dropHolder.getMin(), dropHolder.getMax(), dropHolder.getChance() * chance)); + } + } + }); NpcData.getInstance().getTemplates(npc -> npc.getDropList() != null).forEach(npcTemplate -> { for (DropHolder dropHolder : npcTemplate.getDropList()) diff --git a/L2J_Mobius_03.0_Helios/dist/game/data/xsd/npcs.xsd b/L2J_Mobius_03.0_Helios/dist/game/data/xsd/npcs.xsd index 117ca4cc1d..2c6e583383 100644 --- a/L2J_Mobius_03.0_Helios/dist/game/data/xsd/npcs.xsd +++ b/L2J_Mobius_03.0_Helios/dist/game/data/xsd/npcs.xsd @@ -1,16 +1,5 @@ - - - - - - - - - - - @@ -260,11 +249,81 @@ - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/L2J_Mobius_03.0_Helios/java/org/l2jmobius/gameserver/data/xml/NpcData.java b/L2J_Mobius_03.0_Helios/java/org/l2jmobius/gameserver/data/xml/NpcData.java index 28850ef6bb..d6d5210168 100644 --- a/L2J_Mobius_03.0_Helios/java/org/l2jmobius/gameserver/data/xml/NpcData.java +++ b/L2J_Mobius_03.0_Helios/java/org/l2jmobius/gameserver/data/xml/NpcData.java @@ -46,6 +46,7 @@ import org.l2jmobius.gameserver.enums.MpRewardType; import org.l2jmobius.gameserver.model.StatSet; import org.l2jmobius.gameserver.model.actor.templates.NpcTemplate; import org.l2jmobius.gameserver.model.effects.EffectType; +import org.l2jmobius.gameserver.model.holders.DropGroupHolder; import org.l2jmobius.gameserver.model.holders.DropHolder; import org.l2jmobius.gameserver.model.skill.Skill; @@ -100,6 +101,7 @@ public class NpcData implements IXmlReader Set clans = null; Set ignoreClanNpcIds = null; List dropLists = null; + List dropGroups = null; set.set("id", npcId); set.set("displayId", parseInteger(attrs, "displayId")); set.set("level", parseByte(attrs, "level")); @@ -436,24 +438,54 @@ public class NpcData implements IXmlReader if (dropType != null) { - if (dropLists == null) - { - dropLists = new ArrayList<>(); - } - for (Node dropNode = dropListsNode.getFirstChild(); dropNode != null; dropNode = dropNode.getNextSibling()) { - final NamedNodeMap dropAttrs = dropNode.getAttributes(); - if ("item".equalsIgnoreCase(dropNode.getNodeName())) + final String nodeName = dropNode.getNodeName(); + if (nodeName.equalsIgnoreCase("group")) { - final DropHolder dropItem = new DropHolder(dropType, parseInteger(dropAttrs, "id"), parseLong(dropAttrs, "min"), parseLong(dropAttrs, "max"), parseDouble(dropAttrs, "chance")); - if (ItemTable.getInstance().getTemplate(parseInteger(dropAttrs, "id")) == null) + if (dropGroups == null) { - LOGGER.warning("DropListItem: Could not find item with id " + parseInteger(dropAttrs, "id") + "."); + dropGroups = new ArrayList<>(); + } + + final DropGroupHolder group = new DropGroupHolder(parseDouble(dropNode.getAttributes(), "chance")); + for (Node groupNode = dropNode.getFirstChild(); groupNode != null; groupNode = groupNode.getNextSibling()) + { + if (groupNode.getNodeName().equalsIgnoreCase("item")) + { + final NamedNodeMap groupAttrs = groupNode.getAttributes(); + final int itemId = parseInteger(groupAttrs, "id"); + + if (ItemTable.getInstance().getTemplate(itemId) == null) + { + LOGGER.warning("DropListItem: Could not find item with id " + itemId + "."); + } + else + { + group.addDrop(new DropHolder(dropType, itemId, parseLong(groupAttrs, "min"), parseLong(groupAttrs, "max"), parseDouble(groupAttrs, "chance"))); + } + } + } + + dropGroups.add(group); + } + else if (nodeName.equalsIgnoreCase("item")) + { + if (dropLists == null) + { + dropLists = new ArrayList<>(); + } + + final NamedNodeMap dropAttrs = dropNode.getAttributes(); + final int itemId = parseInteger(dropAttrs, "id"); + + if (ItemTable.getInstance().getTemplate(itemId) == null) + { + LOGGER.warning("DropListItem: Could not find item with id " + itemId + "."); } else { - dropLists.add(dropItem); + dropLists.add(new DropHolder(dropType, itemId, parseLong(dropAttrs, "min"), parseLong(dropAttrs, "max"), parseDouble(dropAttrs, "chance"))); } } } @@ -612,10 +644,21 @@ public class NpcData implements IXmlReader template.setClans(clans); template.setIgnoreClanNpcIds(ignoreClanNpcIds); + // Clean old drop groups. + template.removeDropGroups(); + + // Set new drop groups. + if (dropGroups != null) + { + template.setDropGroups(dropGroups); + } + + // Clean old drop lists. + template.removeDrops(); + + // Set new drop lists. if (dropLists != null) { - template.removeDrops(); - // Drops are sorted by chance (high to low). Collections.sort(dropLists, (d1, d2) -> Double.valueOf(d2.getChance()).compareTo(Double.valueOf(d1.getChance()))); for (DropHolder dropHolder : dropLists) diff --git a/L2J_Mobius_03.0_Helios/java/org/l2jmobius/gameserver/model/actor/templates/NpcTemplate.java b/L2J_Mobius_03.0_Helios/java/org/l2jmobius/gameserver/model/actor/templates/NpcTemplate.java index 3e40f76887..0c282ce418 100644 --- a/L2J_Mobius_03.0_Helios/java/org/l2jmobius/gameserver/model/actor/templates/NpcTemplate.java +++ b/L2J_Mobius_03.0_Helios/java/org/l2jmobius/gameserver/model/actor/templates/NpcTemplate.java @@ -36,6 +36,7 @@ import org.l2jmobius.gameserver.enums.Race; import org.l2jmobius.gameserver.enums.Sex; import org.l2jmobius.gameserver.model.StatSet; import org.l2jmobius.gameserver.model.actor.Creature; +import org.l2jmobius.gameserver.model.holders.DropGroupHolder; import org.l2jmobius.gameserver.model.holders.DropHolder; import org.l2jmobius.gameserver.model.holders.ItemHolder; import org.l2jmobius.gameserver.model.interfaces.IIdentifiable; @@ -107,6 +108,7 @@ public class NpcTemplate extends CreatureTemplate implements IIdentifiable private Map> _aiSkillLists; private Set _clans; private Set _ignoreClanNpcIds; + private List _dropGroups; private List _dropListDeath; private List _dropListSpoil; private float _collisionRadiusGrown; @@ -649,12 +651,22 @@ public class NpcTemplate extends CreatureTemplate implements IIdentifiable _ignoreClanNpcIds = ignoreClanNpcIds != null ? Collections.unmodifiableSet(ignoreClanNpcIds) : null; } + public void removeDropGroups() + { + _dropGroups = null; + } + public void removeDrops() { _dropListDeath = null; _dropListSpoil = null; } + public void setDropGroups(List groups) + { + _dropGroups = groups; + } + public void addDrop(DropHolder dropHolder) { if (_dropListDeath == null) @@ -673,6 +685,11 @@ public class NpcTemplate extends CreatureTemplate implements IIdentifiable _dropListSpoil.add(dropHolder); } + public List getDropGroups() + { + return _dropGroups; + } + public List getDropList() { return _dropListDeath; @@ -685,11 +702,201 @@ public class NpcTemplate extends CreatureTemplate implements IIdentifiable public List calculateDrops(DropType dropType, Creature victim, Creature killer) { - final List dropList = dropType == DropType.SPOIL ? _dropListSpoil : _dropListDeath; - if (dropList == null) + if (dropType == DropType.DROP) { - return null; + // calculate group drops + List groupDrops = null; + if (_dropGroups != null) + { + groupDrops = calculateGroupDrops(victim, killer); + } + + // calculate ungrouped drops + List ungroupedDrops = null; + if (_dropListDeath != null) + { + ungroupedDrops = calculateUngroupedDrops(dropType, victim, killer); + } + + // return results + if ((groupDrops != null) && (ungroupedDrops != null)) + { + groupDrops.addAll(ungroupedDrops); + ungroupedDrops.clear(); + return groupDrops; + } + if (groupDrops != null) + { + return groupDrops; + } + if (ungroupedDrops != null) + { + return ungroupedDrops; + } } + else if ((dropType == DropType.SPOIL) && (_dropListSpoil != null)) + { + return calculateUngroupedDrops(dropType, victim, killer); + } + + // no drops + return null; + } + + private List calculateGroupDrops(Creature victim, Creature killer) + { + // level difference calculations + final int levelDifference = victim.getLevel() - killer.getLevel(); + final double levelGapChanceToDropAdena = Util.map(levelDifference, -Config.DROP_ADENA_MAX_LEVEL_DIFFERENCE, -Config.DROP_ADENA_MIN_LEVEL_DIFFERENCE, Config.DROP_ADENA_MIN_LEVEL_GAP_CHANCE, 100d); + final double levelGapChanceToDrop = Util.map(levelDifference, -Config.DROP_ITEM_MAX_LEVEL_DIFFERENCE, -Config.DROP_ITEM_MIN_LEVEL_DIFFERENCE, Config.DROP_ITEM_MIN_LEVEL_GAP_CHANCE, 100d); + + int dropOccurrenceCounter = victim.isRaid() ? Config.DROP_MAX_OCCURRENCES_RAIDBOSS : Config.DROP_MAX_OCCURRENCES_NORMAL; + List calculatedDrops = null; + for (DropGroupHolder group : _dropGroups) + { + if (dropOccurrenceCounter <= 0) + { + break; + } + + double groupRate = 1; + final List groupDrops = group.getDropList(); + for (DropHolder dropItem : groupDrops) + { + final int itemId = dropItem.getItemId(); + final ItemTemplate item = ItemTable.getInstance().getTemplate(itemId); + final boolean champion = victim.isChampion(); + + // chance + double rateChance = 1; + final Float itemChance = Config.RATE_DROP_CHANCE_BY_ID.get(itemId); + if (itemChance != null) + { + if (itemChance == 0) + { + continue; + } + + rateChance *= itemChance; + if (champion && (itemId == Inventory.ADENA_ID)) + { + rateChance *= Config.CHAMPION_ADENAS_REWARDS_CHANCE; + } + } + else if (item.hasExImmediateEffect()) + { + rateChance *= Config.RATE_HERB_DROP_CHANCE_MULTIPLIER; + } + else if (victim.isRaid()) + { + rateChance *= Config.RATE_RAID_DROP_CHANCE_MULTIPLIER; + } + else + { + rateChance *= Config.RATE_DEATH_DROP_CHANCE_MULTIPLIER * (champion ? Config.CHAMPION_REWARDS_CHANCE : 1); + } + + // premium chance + if (Config.PREMIUM_SYSTEM_ENABLED && (killer.getActingPlayer() != null) && killer.getActingPlayer().hasPremiumStatus()) + { + if (Config.PREMIUM_RATE_DROP_CHANCE_BY_ID.get(itemId) != null) + { + rateChance *= Config.PREMIUM_RATE_DROP_CHANCE_BY_ID.get(itemId); + } + else if (item.hasExImmediateEffect()) + { + // TODO: Premium herb chance? :) + } + else if (victim.isRaid()) + { + // TODO: Premium raid chance? :) + } + else + { + rateChance *= Config.PREMIUM_RATE_DROP_CHANCE; + } + } + + // keep lowest to avoid chance by id configuration conflicts + groupRate = Math.min(groupRate, rateChance); + } + + if ((Rnd.nextDouble() * 100) < (group.getChance() * groupRate)) + { + double totalChance = 0; // total group chance is 100 + final double dropChance = Rnd.nextDouble() * 100; + GROUP_DROP: for (DropHolder dropItem : groupDrops) + { + if (dropOccurrenceCounter <= 0) + { + break; + } + + // calculate if item will drop + totalChance += dropItem.getChance(); + if (dropChance >= totalChance) + { + continue; + } + + // check level gap that may prevent to drop item + if ((Rnd.nextDouble() * 100) > (dropItem.getItemId() == Inventory.ADENA_ID ? levelGapChanceToDropAdena : levelGapChanceToDrop)) + { + continue; + } + + // create the drop + final ItemHolder drop = calculateGroupDrop(dropItem, victim, killer); + + // create list + if (calculatedDrops == null) + { + calculatedDrops = new ArrayList<>(dropOccurrenceCounter); + } + + // finally + if (group.getChance() < 100) + { + dropOccurrenceCounter--; + } + calculatedDrops.add(drop); + + // no more drops from this group + break GROUP_DROP; + } + } + } + + // champion extra drop + if (victim.isChampion()) + { + if ((victim.getLevel() < killer.getLevel()) && (Rnd.get(100) < Config.CHAMPION_REWARD_LOWER_LEVEL_ITEM_CHANCE)) + { + return calculatedDrops; + } + if ((victim.getLevel() > killer.getLevel()) && (Rnd.get(100) < Config.CHAMPION_REWARD_HIGHER_LEVEL_ITEM_CHANCE)) + { + return calculatedDrops; + } + + // create list + if (calculatedDrops == null) + { + calculatedDrops = new ArrayList<>(); + } + + if (!calculatedDrops.containsAll(Config.CHAMPION_REWARD_ITEMS)) + { + calculatedDrops.addAll(Config.CHAMPION_REWARD_ITEMS); + } + } + + return calculatedDrops; + } + + private List calculateUngroupedDrops(DropType dropType, Creature victim, Creature killer) + { + final List dropList = dropType == DropType.SPOIL ? _dropListSpoil : _dropListDeath; // level difference calculations final int levelDifference = victim.getLevel() - killer.getLevel(); @@ -721,7 +928,7 @@ public class NpcTemplate extends CreatureTemplate implements IIdentifiable } // calculate chances - final ItemHolder drop = calculateDrop(dropItem, victim, killer); + final ItemHolder drop = calculateUngroupedDrop(dropItem, victim, killer); if (drop == null) { continue; @@ -776,20 +983,82 @@ public class NpcTemplate extends CreatureTemplate implements IIdentifiable calculatedDrops = new ArrayList<>(); } - calculatedDrops.addAll(Config.CHAMPION_REWARD_ITEMS); + if (!calculatedDrops.containsAll(Config.CHAMPION_REWARD_ITEMS)) + { + calculatedDrops.addAll(Config.CHAMPION_REWARD_ITEMS); + } } return calculatedDrops; } /** - * All item drop chance calculations are done by this method. * @param dropItem * @param victim * @param killer * @return ItemHolder */ - private ItemHolder calculateDrop(DropHolder dropItem, Creature victim, Creature killer) + private ItemHolder calculateGroupDrop(DropHolder dropItem, Creature victim, Creature killer) + { + final int itemId = dropItem.getItemId(); + final ItemTemplate item = ItemTable.getInstance().getTemplate(itemId); + final boolean champion = victim.isChampion(); + + // calculate amount + double rateAmount = 1; + if (Config.RATE_DROP_AMOUNT_BY_ID.get(itemId) != null) + { + rateAmount *= Config.RATE_DROP_AMOUNT_BY_ID.get(itemId); + if (champion && (itemId == Inventory.ADENA_ID)) + { + rateAmount *= Config.CHAMPION_ADENAS_REWARDS_AMOUNT; + } + } + else if (item.hasExImmediateEffect()) + { + rateAmount *= Config.RATE_HERB_DROP_AMOUNT_MULTIPLIER; + } + else if (victim.isRaid()) + { + rateAmount *= Config.RATE_RAID_DROP_AMOUNT_MULTIPLIER; + } + else + { + rateAmount *= Config.RATE_DEATH_DROP_AMOUNT_MULTIPLIER * (champion ? Config.CHAMPION_REWARDS_AMOUNT : 1); + } + + // premium amount + if (Config.PREMIUM_SYSTEM_ENABLED && (killer.getActingPlayer() != null) && killer.getActingPlayer().hasPremiumStatus()) + { + if (Config.PREMIUM_RATE_DROP_AMOUNT_BY_ID.get(itemId) != null) + { + rateAmount *= Config.PREMIUM_RATE_DROP_AMOUNT_BY_ID.get(itemId); + } + else if (item.hasExImmediateEffect()) + { + // TODO: Premium herb amount? :) + } + else if (victim.isRaid()) + { + // TODO: Premium raid amount? :) + } + else + { + rateAmount *= Config.PREMIUM_RATE_DROP_AMOUNT; + } + } + + // finally + return new ItemHolder(itemId, (long) (Rnd.get(dropItem.getMin(), dropItem.getMax()) * rateAmount)); + } + + /** + * @param dropItem + * @param victim + * @param killer + * @return ItemHolder + */ + private ItemHolder calculateUngroupedDrop(DropHolder dropItem, Creature victim, Creature killer) { switch (dropItem.getDropType()) { diff --git a/L2J_Mobius_03.0_Helios/java/org/l2jmobius/gameserver/model/holders/DropGroupHolder.java b/L2J_Mobius_03.0_Helios/java/org/l2jmobius/gameserver/model/holders/DropGroupHolder.java new file mode 100644 index 0000000000..6c2401c0b0 --- /dev/null +++ b/L2J_Mobius_03.0_Helios/java/org/l2jmobius/gameserver/model/holders/DropGroupHolder.java @@ -0,0 +1,49 @@ +/* + * This file is part of the L2J Mobius project. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package org.l2jmobius.gameserver.model.holders; + +import java.util.ArrayList; +import java.util.List; + +/** + * @author Mobius + */ +public class DropGroupHolder +{ + private final List _dropList = new ArrayList<>(); + private final double _chance; + + public DropGroupHolder(double chance) + { + _chance = chance; + } + + public List getDropList() + { + return _dropList; + } + + public void addDrop(DropHolder holder) + { + _dropList.add(holder); + } + + public double getChance() + { + return _chance; + } +} diff --git a/L2J_Mobius_04.0_GrandCrusade/dist/game/data/scripts/handlers/bypasshandlers/NpcViewMod.java b/L2J_Mobius_04.0_GrandCrusade/dist/game/data/scripts/handlers/bypasshandlers/NpcViewMod.java index 35d31315ad..fab79abca8 100644 --- a/L2J_Mobius_04.0_GrandCrusade/dist/game/data/scripts/handlers/bypasshandlers/NpcViewMod.java +++ b/L2J_Mobius_04.0_GrandCrusade/dist/game/data/scripts/handlers/bypasshandlers/NpcViewMod.java @@ -37,6 +37,7 @@ import org.l2jmobius.gameserver.model.actor.Attackable; import org.l2jmobius.gameserver.model.actor.Creature; import org.l2jmobius.gameserver.model.actor.Npc; import org.l2jmobius.gameserver.model.actor.Player; +import org.l2jmobius.gameserver.model.holders.DropGroupHolder; import org.l2jmobius.gameserver.model.holders.DropHolder; import org.l2jmobius.gameserver.model.item.ItemTemplate; import org.l2jmobius.gameserver.model.itemcontainer.Inventory; @@ -326,12 +327,13 @@ public class NpcViewMod implements IBypassHandler private static String getDropListButtons(Npc npc) { final StringBuilder sb = new StringBuilder(); + final List dropListGroups = npc.getTemplate().getDropGroups(); final List dropListDeath = npc.getTemplate().getDropList(); final List dropListSpoil = npc.getTemplate().getSpoilList(); - if ((dropListDeath != null) || (dropListSpoil != null)) + if ((dropListGroups != null) || (dropListDeath != null) || (dropListSpoil != null)) { sb.append("
"); - if (dropListDeath != null) + if ((dropListGroups != null) || (dropListDeath != null)) { sb.append(""); } @@ -348,13 +350,40 @@ public class NpcViewMod implements IBypassHandler private void sendNpcDropList(Player player, Npc npc, DropType dropType, int pageValue) { - final List templateList = dropType == DropType.SPOIL ? npc.getTemplate().getSpoilList() : npc.getTemplate().getDropList(); - if (templateList == null) + List dropList = null; + if (dropType == DropType.SPOIL) + { + dropList = new ArrayList<>(npc.getTemplate().getSpoilList()); + } + else + { + final List drops = npc.getTemplate().getDropList(); + if (drops != null) + { + dropList = new ArrayList<>(drops); + } + final List dropGroups = npc.getTemplate().getDropGroups(); + if (dropGroups != null) + { + if (dropList == null) + { + dropList = new ArrayList<>(); + } + for (DropGroupHolder dropGroup : dropGroups) + { + final double chance = dropGroup.getChance() / 100; + for (DropHolder dropHolder : dropGroup.getDropList()) + { + dropList.add(new DropHolder(dropHolder.getDropType(), dropHolder.getItemId(), dropHolder.getMin(), dropHolder.getMax(), dropHolder.getChance() * chance)); + } + } + } + } + if (dropList == null) { return; } - final List dropList = new ArrayList<>(templateList); Collections.sort(dropList, (d1, d2) -> Integer.valueOf(d1.getItemId()).compareTo(Integer.valueOf(d2.getItemId()))); int pages = dropList.size() / DROP_LIST_ITEMS_PER_PAGE; diff --git a/L2J_Mobius_04.0_GrandCrusade/dist/game/data/scripts/handlers/communityboard/DropSearchBoard.java b/L2J_Mobius_04.0_GrandCrusade/dist/game/data/scripts/handlers/communityboard/DropSearchBoard.java index 647d14cec5..be1d97660b 100644 --- a/L2J_Mobius_04.0_GrandCrusade/dist/game/data/scripts/handlers/communityboard/DropSearchBoard.java +++ b/L2J_Mobius_04.0_GrandCrusade/dist/game/data/scripts/handlers/communityboard/DropSearchBoard.java @@ -36,6 +36,7 @@ import org.l2jmobius.gameserver.handler.CommunityBoardHandler; import org.l2jmobius.gameserver.handler.IParseBoardHandler; import org.l2jmobius.gameserver.model.actor.Player; import org.l2jmobius.gameserver.model.actor.templates.NpcTemplate; +import org.l2jmobius.gameserver.model.holders.DropGroupHolder; import org.l2jmobius.gameserver.model.holders.DropHolder; import org.l2jmobius.gameserver.model.item.ItemTemplate; import org.l2jmobius.gameserver.model.itemcontainer.Inventory; @@ -104,6 +105,17 @@ public class DropSearchBoard implements IParseBoardHandler private void buildDropIndex() { + NpcData.getInstance().getTemplates(npc -> npc.getDropGroups() != null).forEach(npcTemplate -> + { + for (DropGroupHolder dropGroup : npcTemplate.getDropGroups()) + { + final double chance = dropGroup.getChance() / 100; + for (DropHolder dropHolder : dropGroup.getDropList()) + { + addToDropList(npcTemplate, new DropHolder(dropHolder.getDropType(), dropHolder.getItemId(), dropHolder.getMin(), dropHolder.getMax(), dropHolder.getChance() * chance)); + } + } + }); NpcData.getInstance().getTemplates(npc -> npc.getDropList() != null).forEach(npcTemplate -> { for (DropHolder dropHolder : npcTemplate.getDropList()) diff --git a/L2J_Mobius_04.0_GrandCrusade/dist/game/data/xsd/npcs.xsd b/L2J_Mobius_04.0_GrandCrusade/dist/game/data/xsd/npcs.xsd index 117ca4cc1d..2c6e583383 100644 --- a/L2J_Mobius_04.0_GrandCrusade/dist/game/data/xsd/npcs.xsd +++ b/L2J_Mobius_04.0_GrandCrusade/dist/game/data/xsd/npcs.xsd @@ -1,16 +1,5 @@ - - - - - - - - - - - @@ -260,11 +249,81 @@ - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/L2J_Mobius_04.0_GrandCrusade/java/org/l2jmobius/gameserver/data/xml/NpcData.java b/L2J_Mobius_04.0_GrandCrusade/java/org/l2jmobius/gameserver/data/xml/NpcData.java index 28850ef6bb..d6d5210168 100644 --- a/L2J_Mobius_04.0_GrandCrusade/java/org/l2jmobius/gameserver/data/xml/NpcData.java +++ b/L2J_Mobius_04.0_GrandCrusade/java/org/l2jmobius/gameserver/data/xml/NpcData.java @@ -46,6 +46,7 @@ import org.l2jmobius.gameserver.enums.MpRewardType; import org.l2jmobius.gameserver.model.StatSet; import org.l2jmobius.gameserver.model.actor.templates.NpcTemplate; import org.l2jmobius.gameserver.model.effects.EffectType; +import org.l2jmobius.gameserver.model.holders.DropGroupHolder; import org.l2jmobius.gameserver.model.holders.DropHolder; import org.l2jmobius.gameserver.model.skill.Skill; @@ -100,6 +101,7 @@ public class NpcData implements IXmlReader Set clans = null; Set ignoreClanNpcIds = null; List dropLists = null; + List dropGroups = null; set.set("id", npcId); set.set("displayId", parseInteger(attrs, "displayId")); set.set("level", parseByte(attrs, "level")); @@ -436,24 +438,54 @@ public class NpcData implements IXmlReader if (dropType != null) { - if (dropLists == null) - { - dropLists = new ArrayList<>(); - } - for (Node dropNode = dropListsNode.getFirstChild(); dropNode != null; dropNode = dropNode.getNextSibling()) { - final NamedNodeMap dropAttrs = dropNode.getAttributes(); - if ("item".equalsIgnoreCase(dropNode.getNodeName())) + final String nodeName = dropNode.getNodeName(); + if (nodeName.equalsIgnoreCase("group")) { - final DropHolder dropItem = new DropHolder(dropType, parseInteger(dropAttrs, "id"), parseLong(dropAttrs, "min"), parseLong(dropAttrs, "max"), parseDouble(dropAttrs, "chance")); - if (ItemTable.getInstance().getTemplate(parseInteger(dropAttrs, "id")) == null) + if (dropGroups == null) { - LOGGER.warning("DropListItem: Could not find item with id " + parseInteger(dropAttrs, "id") + "."); + dropGroups = new ArrayList<>(); + } + + final DropGroupHolder group = new DropGroupHolder(parseDouble(dropNode.getAttributes(), "chance")); + for (Node groupNode = dropNode.getFirstChild(); groupNode != null; groupNode = groupNode.getNextSibling()) + { + if (groupNode.getNodeName().equalsIgnoreCase("item")) + { + final NamedNodeMap groupAttrs = groupNode.getAttributes(); + final int itemId = parseInteger(groupAttrs, "id"); + + if (ItemTable.getInstance().getTemplate(itemId) == null) + { + LOGGER.warning("DropListItem: Could not find item with id " + itemId + "."); + } + else + { + group.addDrop(new DropHolder(dropType, itemId, parseLong(groupAttrs, "min"), parseLong(groupAttrs, "max"), parseDouble(groupAttrs, "chance"))); + } + } + } + + dropGroups.add(group); + } + else if (nodeName.equalsIgnoreCase("item")) + { + if (dropLists == null) + { + dropLists = new ArrayList<>(); + } + + final NamedNodeMap dropAttrs = dropNode.getAttributes(); + final int itemId = parseInteger(dropAttrs, "id"); + + if (ItemTable.getInstance().getTemplate(itemId) == null) + { + LOGGER.warning("DropListItem: Could not find item with id " + itemId + "."); } else { - dropLists.add(dropItem); + dropLists.add(new DropHolder(dropType, itemId, parseLong(dropAttrs, "min"), parseLong(dropAttrs, "max"), parseDouble(dropAttrs, "chance"))); } } } @@ -612,10 +644,21 @@ public class NpcData implements IXmlReader template.setClans(clans); template.setIgnoreClanNpcIds(ignoreClanNpcIds); + // Clean old drop groups. + template.removeDropGroups(); + + // Set new drop groups. + if (dropGroups != null) + { + template.setDropGroups(dropGroups); + } + + // Clean old drop lists. + template.removeDrops(); + + // Set new drop lists. if (dropLists != null) { - template.removeDrops(); - // Drops are sorted by chance (high to low). Collections.sort(dropLists, (d1, d2) -> Double.valueOf(d2.getChance()).compareTo(Double.valueOf(d1.getChance()))); for (DropHolder dropHolder : dropLists) diff --git a/L2J_Mobius_04.0_GrandCrusade/java/org/l2jmobius/gameserver/model/actor/templates/NpcTemplate.java b/L2J_Mobius_04.0_GrandCrusade/java/org/l2jmobius/gameserver/model/actor/templates/NpcTemplate.java index 3e40f76887..0c282ce418 100644 --- a/L2J_Mobius_04.0_GrandCrusade/java/org/l2jmobius/gameserver/model/actor/templates/NpcTemplate.java +++ b/L2J_Mobius_04.0_GrandCrusade/java/org/l2jmobius/gameserver/model/actor/templates/NpcTemplate.java @@ -36,6 +36,7 @@ import org.l2jmobius.gameserver.enums.Race; import org.l2jmobius.gameserver.enums.Sex; import org.l2jmobius.gameserver.model.StatSet; import org.l2jmobius.gameserver.model.actor.Creature; +import org.l2jmobius.gameserver.model.holders.DropGroupHolder; import org.l2jmobius.gameserver.model.holders.DropHolder; import org.l2jmobius.gameserver.model.holders.ItemHolder; import org.l2jmobius.gameserver.model.interfaces.IIdentifiable; @@ -107,6 +108,7 @@ public class NpcTemplate extends CreatureTemplate implements IIdentifiable private Map> _aiSkillLists; private Set _clans; private Set _ignoreClanNpcIds; + private List _dropGroups; private List _dropListDeath; private List _dropListSpoil; private float _collisionRadiusGrown; @@ -649,12 +651,22 @@ public class NpcTemplate extends CreatureTemplate implements IIdentifiable _ignoreClanNpcIds = ignoreClanNpcIds != null ? Collections.unmodifiableSet(ignoreClanNpcIds) : null; } + public void removeDropGroups() + { + _dropGroups = null; + } + public void removeDrops() { _dropListDeath = null; _dropListSpoil = null; } + public void setDropGroups(List groups) + { + _dropGroups = groups; + } + public void addDrop(DropHolder dropHolder) { if (_dropListDeath == null) @@ -673,6 +685,11 @@ public class NpcTemplate extends CreatureTemplate implements IIdentifiable _dropListSpoil.add(dropHolder); } + public List getDropGroups() + { + return _dropGroups; + } + public List getDropList() { return _dropListDeath; @@ -685,11 +702,201 @@ public class NpcTemplate extends CreatureTemplate implements IIdentifiable public List calculateDrops(DropType dropType, Creature victim, Creature killer) { - final List dropList = dropType == DropType.SPOIL ? _dropListSpoil : _dropListDeath; - if (dropList == null) + if (dropType == DropType.DROP) { - return null; + // calculate group drops + List groupDrops = null; + if (_dropGroups != null) + { + groupDrops = calculateGroupDrops(victim, killer); + } + + // calculate ungrouped drops + List ungroupedDrops = null; + if (_dropListDeath != null) + { + ungroupedDrops = calculateUngroupedDrops(dropType, victim, killer); + } + + // return results + if ((groupDrops != null) && (ungroupedDrops != null)) + { + groupDrops.addAll(ungroupedDrops); + ungroupedDrops.clear(); + return groupDrops; + } + if (groupDrops != null) + { + return groupDrops; + } + if (ungroupedDrops != null) + { + return ungroupedDrops; + } } + else if ((dropType == DropType.SPOIL) && (_dropListSpoil != null)) + { + return calculateUngroupedDrops(dropType, victim, killer); + } + + // no drops + return null; + } + + private List calculateGroupDrops(Creature victim, Creature killer) + { + // level difference calculations + final int levelDifference = victim.getLevel() - killer.getLevel(); + final double levelGapChanceToDropAdena = Util.map(levelDifference, -Config.DROP_ADENA_MAX_LEVEL_DIFFERENCE, -Config.DROP_ADENA_MIN_LEVEL_DIFFERENCE, Config.DROP_ADENA_MIN_LEVEL_GAP_CHANCE, 100d); + final double levelGapChanceToDrop = Util.map(levelDifference, -Config.DROP_ITEM_MAX_LEVEL_DIFFERENCE, -Config.DROP_ITEM_MIN_LEVEL_DIFFERENCE, Config.DROP_ITEM_MIN_LEVEL_GAP_CHANCE, 100d); + + int dropOccurrenceCounter = victim.isRaid() ? Config.DROP_MAX_OCCURRENCES_RAIDBOSS : Config.DROP_MAX_OCCURRENCES_NORMAL; + List calculatedDrops = null; + for (DropGroupHolder group : _dropGroups) + { + if (dropOccurrenceCounter <= 0) + { + break; + } + + double groupRate = 1; + final List groupDrops = group.getDropList(); + for (DropHolder dropItem : groupDrops) + { + final int itemId = dropItem.getItemId(); + final ItemTemplate item = ItemTable.getInstance().getTemplate(itemId); + final boolean champion = victim.isChampion(); + + // chance + double rateChance = 1; + final Float itemChance = Config.RATE_DROP_CHANCE_BY_ID.get(itemId); + if (itemChance != null) + { + if (itemChance == 0) + { + continue; + } + + rateChance *= itemChance; + if (champion && (itemId == Inventory.ADENA_ID)) + { + rateChance *= Config.CHAMPION_ADENAS_REWARDS_CHANCE; + } + } + else if (item.hasExImmediateEffect()) + { + rateChance *= Config.RATE_HERB_DROP_CHANCE_MULTIPLIER; + } + else if (victim.isRaid()) + { + rateChance *= Config.RATE_RAID_DROP_CHANCE_MULTIPLIER; + } + else + { + rateChance *= Config.RATE_DEATH_DROP_CHANCE_MULTIPLIER * (champion ? Config.CHAMPION_REWARDS_CHANCE : 1); + } + + // premium chance + if (Config.PREMIUM_SYSTEM_ENABLED && (killer.getActingPlayer() != null) && killer.getActingPlayer().hasPremiumStatus()) + { + if (Config.PREMIUM_RATE_DROP_CHANCE_BY_ID.get(itemId) != null) + { + rateChance *= Config.PREMIUM_RATE_DROP_CHANCE_BY_ID.get(itemId); + } + else if (item.hasExImmediateEffect()) + { + // TODO: Premium herb chance? :) + } + else if (victim.isRaid()) + { + // TODO: Premium raid chance? :) + } + else + { + rateChance *= Config.PREMIUM_RATE_DROP_CHANCE; + } + } + + // keep lowest to avoid chance by id configuration conflicts + groupRate = Math.min(groupRate, rateChance); + } + + if ((Rnd.nextDouble() * 100) < (group.getChance() * groupRate)) + { + double totalChance = 0; // total group chance is 100 + final double dropChance = Rnd.nextDouble() * 100; + GROUP_DROP: for (DropHolder dropItem : groupDrops) + { + if (dropOccurrenceCounter <= 0) + { + break; + } + + // calculate if item will drop + totalChance += dropItem.getChance(); + if (dropChance >= totalChance) + { + continue; + } + + // check level gap that may prevent to drop item + if ((Rnd.nextDouble() * 100) > (dropItem.getItemId() == Inventory.ADENA_ID ? levelGapChanceToDropAdena : levelGapChanceToDrop)) + { + continue; + } + + // create the drop + final ItemHolder drop = calculateGroupDrop(dropItem, victim, killer); + + // create list + if (calculatedDrops == null) + { + calculatedDrops = new ArrayList<>(dropOccurrenceCounter); + } + + // finally + if (group.getChance() < 100) + { + dropOccurrenceCounter--; + } + calculatedDrops.add(drop); + + // no more drops from this group + break GROUP_DROP; + } + } + } + + // champion extra drop + if (victim.isChampion()) + { + if ((victim.getLevel() < killer.getLevel()) && (Rnd.get(100) < Config.CHAMPION_REWARD_LOWER_LEVEL_ITEM_CHANCE)) + { + return calculatedDrops; + } + if ((victim.getLevel() > killer.getLevel()) && (Rnd.get(100) < Config.CHAMPION_REWARD_HIGHER_LEVEL_ITEM_CHANCE)) + { + return calculatedDrops; + } + + // create list + if (calculatedDrops == null) + { + calculatedDrops = new ArrayList<>(); + } + + if (!calculatedDrops.containsAll(Config.CHAMPION_REWARD_ITEMS)) + { + calculatedDrops.addAll(Config.CHAMPION_REWARD_ITEMS); + } + } + + return calculatedDrops; + } + + private List calculateUngroupedDrops(DropType dropType, Creature victim, Creature killer) + { + final List dropList = dropType == DropType.SPOIL ? _dropListSpoil : _dropListDeath; // level difference calculations final int levelDifference = victim.getLevel() - killer.getLevel(); @@ -721,7 +928,7 @@ public class NpcTemplate extends CreatureTemplate implements IIdentifiable } // calculate chances - final ItemHolder drop = calculateDrop(dropItem, victim, killer); + final ItemHolder drop = calculateUngroupedDrop(dropItem, victim, killer); if (drop == null) { continue; @@ -776,20 +983,82 @@ public class NpcTemplate extends CreatureTemplate implements IIdentifiable calculatedDrops = new ArrayList<>(); } - calculatedDrops.addAll(Config.CHAMPION_REWARD_ITEMS); + if (!calculatedDrops.containsAll(Config.CHAMPION_REWARD_ITEMS)) + { + calculatedDrops.addAll(Config.CHAMPION_REWARD_ITEMS); + } } return calculatedDrops; } /** - * All item drop chance calculations are done by this method. * @param dropItem * @param victim * @param killer * @return ItemHolder */ - private ItemHolder calculateDrop(DropHolder dropItem, Creature victim, Creature killer) + private ItemHolder calculateGroupDrop(DropHolder dropItem, Creature victim, Creature killer) + { + final int itemId = dropItem.getItemId(); + final ItemTemplate item = ItemTable.getInstance().getTemplate(itemId); + final boolean champion = victim.isChampion(); + + // calculate amount + double rateAmount = 1; + if (Config.RATE_DROP_AMOUNT_BY_ID.get(itemId) != null) + { + rateAmount *= Config.RATE_DROP_AMOUNT_BY_ID.get(itemId); + if (champion && (itemId == Inventory.ADENA_ID)) + { + rateAmount *= Config.CHAMPION_ADENAS_REWARDS_AMOUNT; + } + } + else if (item.hasExImmediateEffect()) + { + rateAmount *= Config.RATE_HERB_DROP_AMOUNT_MULTIPLIER; + } + else if (victim.isRaid()) + { + rateAmount *= Config.RATE_RAID_DROP_AMOUNT_MULTIPLIER; + } + else + { + rateAmount *= Config.RATE_DEATH_DROP_AMOUNT_MULTIPLIER * (champion ? Config.CHAMPION_REWARDS_AMOUNT : 1); + } + + // premium amount + if (Config.PREMIUM_SYSTEM_ENABLED && (killer.getActingPlayer() != null) && killer.getActingPlayer().hasPremiumStatus()) + { + if (Config.PREMIUM_RATE_DROP_AMOUNT_BY_ID.get(itemId) != null) + { + rateAmount *= Config.PREMIUM_RATE_DROP_AMOUNT_BY_ID.get(itemId); + } + else if (item.hasExImmediateEffect()) + { + // TODO: Premium herb amount? :) + } + else if (victim.isRaid()) + { + // TODO: Premium raid amount? :) + } + else + { + rateAmount *= Config.PREMIUM_RATE_DROP_AMOUNT; + } + } + + // finally + return new ItemHolder(itemId, (long) (Rnd.get(dropItem.getMin(), dropItem.getMax()) * rateAmount)); + } + + /** + * @param dropItem + * @param victim + * @param killer + * @return ItemHolder + */ + private ItemHolder calculateUngroupedDrop(DropHolder dropItem, Creature victim, Creature killer) { switch (dropItem.getDropType()) { diff --git a/L2J_Mobius_04.0_GrandCrusade/java/org/l2jmobius/gameserver/model/holders/DropGroupHolder.java b/L2J_Mobius_04.0_GrandCrusade/java/org/l2jmobius/gameserver/model/holders/DropGroupHolder.java new file mode 100644 index 0000000000..6c2401c0b0 --- /dev/null +++ b/L2J_Mobius_04.0_GrandCrusade/java/org/l2jmobius/gameserver/model/holders/DropGroupHolder.java @@ -0,0 +1,49 @@ +/* + * This file is part of the L2J Mobius project. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package org.l2jmobius.gameserver.model.holders; + +import java.util.ArrayList; +import java.util.List; + +/** + * @author Mobius + */ +public class DropGroupHolder +{ + private final List _dropList = new ArrayList<>(); + private final double _chance; + + public DropGroupHolder(double chance) + { + _chance = chance; + } + + public List getDropList() + { + return _dropList; + } + + public void addDrop(DropHolder holder) + { + _dropList.add(holder); + } + + public double getChance() + { + return _chance; + } +} diff --git a/L2J_Mobius_05.0_Salvation/dist/game/data/scripts/handlers/bypasshandlers/NpcViewMod.java b/L2J_Mobius_05.0_Salvation/dist/game/data/scripts/handlers/bypasshandlers/NpcViewMod.java index 35d31315ad..fab79abca8 100644 --- a/L2J_Mobius_05.0_Salvation/dist/game/data/scripts/handlers/bypasshandlers/NpcViewMod.java +++ b/L2J_Mobius_05.0_Salvation/dist/game/data/scripts/handlers/bypasshandlers/NpcViewMod.java @@ -37,6 +37,7 @@ import org.l2jmobius.gameserver.model.actor.Attackable; import org.l2jmobius.gameserver.model.actor.Creature; import org.l2jmobius.gameserver.model.actor.Npc; import org.l2jmobius.gameserver.model.actor.Player; +import org.l2jmobius.gameserver.model.holders.DropGroupHolder; import org.l2jmobius.gameserver.model.holders.DropHolder; import org.l2jmobius.gameserver.model.item.ItemTemplate; import org.l2jmobius.gameserver.model.itemcontainer.Inventory; @@ -326,12 +327,13 @@ public class NpcViewMod implements IBypassHandler private static String getDropListButtons(Npc npc) { final StringBuilder sb = new StringBuilder(); + final List dropListGroups = npc.getTemplate().getDropGroups(); final List dropListDeath = npc.getTemplate().getDropList(); final List dropListSpoil = npc.getTemplate().getSpoilList(); - if ((dropListDeath != null) || (dropListSpoil != null)) + if ((dropListGroups != null) || (dropListDeath != null) || (dropListSpoil != null)) { sb.append("
"); - if (dropListDeath != null) + if ((dropListGroups != null) || (dropListDeath != null)) { sb.append(""); } @@ -348,13 +350,40 @@ public class NpcViewMod implements IBypassHandler private void sendNpcDropList(Player player, Npc npc, DropType dropType, int pageValue) { - final List templateList = dropType == DropType.SPOIL ? npc.getTemplate().getSpoilList() : npc.getTemplate().getDropList(); - if (templateList == null) + List dropList = null; + if (dropType == DropType.SPOIL) + { + dropList = new ArrayList<>(npc.getTemplate().getSpoilList()); + } + else + { + final List drops = npc.getTemplate().getDropList(); + if (drops != null) + { + dropList = new ArrayList<>(drops); + } + final List dropGroups = npc.getTemplate().getDropGroups(); + if (dropGroups != null) + { + if (dropList == null) + { + dropList = new ArrayList<>(); + } + for (DropGroupHolder dropGroup : dropGroups) + { + final double chance = dropGroup.getChance() / 100; + for (DropHolder dropHolder : dropGroup.getDropList()) + { + dropList.add(new DropHolder(dropHolder.getDropType(), dropHolder.getItemId(), dropHolder.getMin(), dropHolder.getMax(), dropHolder.getChance() * chance)); + } + } + } + } + if (dropList == null) { return; } - final List dropList = new ArrayList<>(templateList); Collections.sort(dropList, (d1, d2) -> Integer.valueOf(d1.getItemId()).compareTo(Integer.valueOf(d2.getItemId()))); int pages = dropList.size() / DROP_LIST_ITEMS_PER_PAGE; diff --git a/L2J_Mobius_05.0_Salvation/dist/game/data/scripts/handlers/communityboard/DropSearchBoard.java b/L2J_Mobius_05.0_Salvation/dist/game/data/scripts/handlers/communityboard/DropSearchBoard.java index 647d14cec5..be1d97660b 100644 --- a/L2J_Mobius_05.0_Salvation/dist/game/data/scripts/handlers/communityboard/DropSearchBoard.java +++ b/L2J_Mobius_05.0_Salvation/dist/game/data/scripts/handlers/communityboard/DropSearchBoard.java @@ -36,6 +36,7 @@ import org.l2jmobius.gameserver.handler.CommunityBoardHandler; import org.l2jmobius.gameserver.handler.IParseBoardHandler; import org.l2jmobius.gameserver.model.actor.Player; import org.l2jmobius.gameserver.model.actor.templates.NpcTemplate; +import org.l2jmobius.gameserver.model.holders.DropGroupHolder; import org.l2jmobius.gameserver.model.holders.DropHolder; import org.l2jmobius.gameserver.model.item.ItemTemplate; import org.l2jmobius.gameserver.model.itemcontainer.Inventory; @@ -104,6 +105,17 @@ public class DropSearchBoard implements IParseBoardHandler private void buildDropIndex() { + NpcData.getInstance().getTemplates(npc -> npc.getDropGroups() != null).forEach(npcTemplate -> + { + for (DropGroupHolder dropGroup : npcTemplate.getDropGroups()) + { + final double chance = dropGroup.getChance() / 100; + for (DropHolder dropHolder : dropGroup.getDropList()) + { + addToDropList(npcTemplate, new DropHolder(dropHolder.getDropType(), dropHolder.getItemId(), dropHolder.getMin(), dropHolder.getMax(), dropHolder.getChance() * chance)); + } + } + }); NpcData.getInstance().getTemplates(npc -> npc.getDropList() != null).forEach(npcTemplate -> { for (DropHolder dropHolder : npcTemplate.getDropList()) diff --git a/L2J_Mobius_05.0_Salvation/dist/game/data/xsd/npcs.xsd b/L2J_Mobius_05.0_Salvation/dist/game/data/xsd/npcs.xsd index 117ca4cc1d..2c6e583383 100644 --- a/L2J_Mobius_05.0_Salvation/dist/game/data/xsd/npcs.xsd +++ b/L2J_Mobius_05.0_Salvation/dist/game/data/xsd/npcs.xsd @@ -1,16 +1,5 @@ - - - - - - - - - - - @@ -260,11 +249,81 @@ - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/L2J_Mobius_05.0_Salvation/java/org/l2jmobius/gameserver/data/xml/NpcData.java b/L2J_Mobius_05.0_Salvation/java/org/l2jmobius/gameserver/data/xml/NpcData.java index 28850ef6bb..d6d5210168 100644 --- a/L2J_Mobius_05.0_Salvation/java/org/l2jmobius/gameserver/data/xml/NpcData.java +++ b/L2J_Mobius_05.0_Salvation/java/org/l2jmobius/gameserver/data/xml/NpcData.java @@ -46,6 +46,7 @@ import org.l2jmobius.gameserver.enums.MpRewardType; import org.l2jmobius.gameserver.model.StatSet; import org.l2jmobius.gameserver.model.actor.templates.NpcTemplate; import org.l2jmobius.gameserver.model.effects.EffectType; +import org.l2jmobius.gameserver.model.holders.DropGroupHolder; import org.l2jmobius.gameserver.model.holders.DropHolder; import org.l2jmobius.gameserver.model.skill.Skill; @@ -100,6 +101,7 @@ public class NpcData implements IXmlReader Set clans = null; Set ignoreClanNpcIds = null; List dropLists = null; + List dropGroups = null; set.set("id", npcId); set.set("displayId", parseInteger(attrs, "displayId")); set.set("level", parseByte(attrs, "level")); @@ -436,24 +438,54 @@ public class NpcData implements IXmlReader if (dropType != null) { - if (dropLists == null) - { - dropLists = new ArrayList<>(); - } - for (Node dropNode = dropListsNode.getFirstChild(); dropNode != null; dropNode = dropNode.getNextSibling()) { - final NamedNodeMap dropAttrs = dropNode.getAttributes(); - if ("item".equalsIgnoreCase(dropNode.getNodeName())) + final String nodeName = dropNode.getNodeName(); + if (nodeName.equalsIgnoreCase("group")) { - final DropHolder dropItem = new DropHolder(dropType, parseInteger(dropAttrs, "id"), parseLong(dropAttrs, "min"), parseLong(dropAttrs, "max"), parseDouble(dropAttrs, "chance")); - if (ItemTable.getInstance().getTemplate(parseInteger(dropAttrs, "id")) == null) + if (dropGroups == null) { - LOGGER.warning("DropListItem: Could not find item with id " + parseInteger(dropAttrs, "id") + "."); + dropGroups = new ArrayList<>(); + } + + final DropGroupHolder group = new DropGroupHolder(parseDouble(dropNode.getAttributes(), "chance")); + for (Node groupNode = dropNode.getFirstChild(); groupNode != null; groupNode = groupNode.getNextSibling()) + { + if (groupNode.getNodeName().equalsIgnoreCase("item")) + { + final NamedNodeMap groupAttrs = groupNode.getAttributes(); + final int itemId = parseInteger(groupAttrs, "id"); + + if (ItemTable.getInstance().getTemplate(itemId) == null) + { + LOGGER.warning("DropListItem: Could not find item with id " + itemId + "."); + } + else + { + group.addDrop(new DropHolder(dropType, itemId, parseLong(groupAttrs, "min"), parseLong(groupAttrs, "max"), parseDouble(groupAttrs, "chance"))); + } + } + } + + dropGroups.add(group); + } + else if (nodeName.equalsIgnoreCase("item")) + { + if (dropLists == null) + { + dropLists = new ArrayList<>(); + } + + final NamedNodeMap dropAttrs = dropNode.getAttributes(); + final int itemId = parseInteger(dropAttrs, "id"); + + if (ItemTable.getInstance().getTemplate(itemId) == null) + { + LOGGER.warning("DropListItem: Could not find item with id " + itemId + "."); } else { - dropLists.add(dropItem); + dropLists.add(new DropHolder(dropType, itemId, parseLong(dropAttrs, "min"), parseLong(dropAttrs, "max"), parseDouble(dropAttrs, "chance"))); } } } @@ -612,10 +644,21 @@ public class NpcData implements IXmlReader template.setClans(clans); template.setIgnoreClanNpcIds(ignoreClanNpcIds); + // Clean old drop groups. + template.removeDropGroups(); + + // Set new drop groups. + if (dropGroups != null) + { + template.setDropGroups(dropGroups); + } + + // Clean old drop lists. + template.removeDrops(); + + // Set new drop lists. if (dropLists != null) { - template.removeDrops(); - // Drops are sorted by chance (high to low). Collections.sort(dropLists, (d1, d2) -> Double.valueOf(d2.getChance()).compareTo(Double.valueOf(d1.getChance()))); for (DropHolder dropHolder : dropLists) diff --git a/L2J_Mobius_05.0_Salvation/java/org/l2jmobius/gameserver/model/actor/templates/NpcTemplate.java b/L2J_Mobius_05.0_Salvation/java/org/l2jmobius/gameserver/model/actor/templates/NpcTemplate.java index 3e40f76887..0c282ce418 100644 --- a/L2J_Mobius_05.0_Salvation/java/org/l2jmobius/gameserver/model/actor/templates/NpcTemplate.java +++ b/L2J_Mobius_05.0_Salvation/java/org/l2jmobius/gameserver/model/actor/templates/NpcTemplate.java @@ -36,6 +36,7 @@ import org.l2jmobius.gameserver.enums.Race; import org.l2jmobius.gameserver.enums.Sex; import org.l2jmobius.gameserver.model.StatSet; import org.l2jmobius.gameserver.model.actor.Creature; +import org.l2jmobius.gameserver.model.holders.DropGroupHolder; import org.l2jmobius.gameserver.model.holders.DropHolder; import org.l2jmobius.gameserver.model.holders.ItemHolder; import org.l2jmobius.gameserver.model.interfaces.IIdentifiable; @@ -107,6 +108,7 @@ public class NpcTemplate extends CreatureTemplate implements IIdentifiable private Map> _aiSkillLists; private Set _clans; private Set _ignoreClanNpcIds; + private List _dropGroups; private List _dropListDeath; private List _dropListSpoil; private float _collisionRadiusGrown; @@ -649,12 +651,22 @@ public class NpcTemplate extends CreatureTemplate implements IIdentifiable _ignoreClanNpcIds = ignoreClanNpcIds != null ? Collections.unmodifiableSet(ignoreClanNpcIds) : null; } + public void removeDropGroups() + { + _dropGroups = null; + } + public void removeDrops() { _dropListDeath = null; _dropListSpoil = null; } + public void setDropGroups(List groups) + { + _dropGroups = groups; + } + public void addDrop(DropHolder dropHolder) { if (_dropListDeath == null) @@ -673,6 +685,11 @@ public class NpcTemplate extends CreatureTemplate implements IIdentifiable _dropListSpoil.add(dropHolder); } + public List getDropGroups() + { + return _dropGroups; + } + public List getDropList() { return _dropListDeath; @@ -685,11 +702,201 @@ public class NpcTemplate extends CreatureTemplate implements IIdentifiable public List calculateDrops(DropType dropType, Creature victim, Creature killer) { - final List dropList = dropType == DropType.SPOIL ? _dropListSpoil : _dropListDeath; - if (dropList == null) + if (dropType == DropType.DROP) { - return null; + // calculate group drops + List groupDrops = null; + if (_dropGroups != null) + { + groupDrops = calculateGroupDrops(victim, killer); + } + + // calculate ungrouped drops + List ungroupedDrops = null; + if (_dropListDeath != null) + { + ungroupedDrops = calculateUngroupedDrops(dropType, victim, killer); + } + + // return results + if ((groupDrops != null) && (ungroupedDrops != null)) + { + groupDrops.addAll(ungroupedDrops); + ungroupedDrops.clear(); + return groupDrops; + } + if (groupDrops != null) + { + return groupDrops; + } + if (ungroupedDrops != null) + { + return ungroupedDrops; + } } + else if ((dropType == DropType.SPOIL) && (_dropListSpoil != null)) + { + return calculateUngroupedDrops(dropType, victim, killer); + } + + // no drops + return null; + } + + private List calculateGroupDrops(Creature victim, Creature killer) + { + // level difference calculations + final int levelDifference = victim.getLevel() - killer.getLevel(); + final double levelGapChanceToDropAdena = Util.map(levelDifference, -Config.DROP_ADENA_MAX_LEVEL_DIFFERENCE, -Config.DROP_ADENA_MIN_LEVEL_DIFFERENCE, Config.DROP_ADENA_MIN_LEVEL_GAP_CHANCE, 100d); + final double levelGapChanceToDrop = Util.map(levelDifference, -Config.DROP_ITEM_MAX_LEVEL_DIFFERENCE, -Config.DROP_ITEM_MIN_LEVEL_DIFFERENCE, Config.DROP_ITEM_MIN_LEVEL_GAP_CHANCE, 100d); + + int dropOccurrenceCounter = victim.isRaid() ? Config.DROP_MAX_OCCURRENCES_RAIDBOSS : Config.DROP_MAX_OCCURRENCES_NORMAL; + List calculatedDrops = null; + for (DropGroupHolder group : _dropGroups) + { + if (dropOccurrenceCounter <= 0) + { + break; + } + + double groupRate = 1; + final List groupDrops = group.getDropList(); + for (DropHolder dropItem : groupDrops) + { + final int itemId = dropItem.getItemId(); + final ItemTemplate item = ItemTable.getInstance().getTemplate(itemId); + final boolean champion = victim.isChampion(); + + // chance + double rateChance = 1; + final Float itemChance = Config.RATE_DROP_CHANCE_BY_ID.get(itemId); + if (itemChance != null) + { + if (itemChance == 0) + { + continue; + } + + rateChance *= itemChance; + if (champion && (itemId == Inventory.ADENA_ID)) + { + rateChance *= Config.CHAMPION_ADENAS_REWARDS_CHANCE; + } + } + else if (item.hasExImmediateEffect()) + { + rateChance *= Config.RATE_HERB_DROP_CHANCE_MULTIPLIER; + } + else if (victim.isRaid()) + { + rateChance *= Config.RATE_RAID_DROP_CHANCE_MULTIPLIER; + } + else + { + rateChance *= Config.RATE_DEATH_DROP_CHANCE_MULTIPLIER * (champion ? Config.CHAMPION_REWARDS_CHANCE : 1); + } + + // premium chance + if (Config.PREMIUM_SYSTEM_ENABLED && (killer.getActingPlayer() != null) && killer.getActingPlayer().hasPremiumStatus()) + { + if (Config.PREMIUM_RATE_DROP_CHANCE_BY_ID.get(itemId) != null) + { + rateChance *= Config.PREMIUM_RATE_DROP_CHANCE_BY_ID.get(itemId); + } + else if (item.hasExImmediateEffect()) + { + // TODO: Premium herb chance? :) + } + else if (victim.isRaid()) + { + // TODO: Premium raid chance? :) + } + else + { + rateChance *= Config.PREMIUM_RATE_DROP_CHANCE; + } + } + + // keep lowest to avoid chance by id configuration conflicts + groupRate = Math.min(groupRate, rateChance); + } + + if ((Rnd.nextDouble() * 100) < (group.getChance() * groupRate)) + { + double totalChance = 0; // total group chance is 100 + final double dropChance = Rnd.nextDouble() * 100; + GROUP_DROP: for (DropHolder dropItem : groupDrops) + { + if (dropOccurrenceCounter <= 0) + { + break; + } + + // calculate if item will drop + totalChance += dropItem.getChance(); + if (dropChance >= totalChance) + { + continue; + } + + // check level gap that may prevent to drop item + if ((Rnd.nextDouble() * 100) > (dropItem.getItemId() == Inventory.ADENA_ID ? levelGapChanceToDropAdena : levelGapChanceToDrop)) + { + continue; + } + + // create the drop + final ItemHolder drop = calculateGroupDrop(dropItem, victim, killer); + + // create list + if (calculatedDrops == null) + { + calculatedDrops = new ArrayList<>(dropOccurrenceCounter); + } + + // finally + if (group.getChance() < 100) + { + dropOccurrenceCounter--; + } + calculatedDrops.add(drop); + + // no more drops from this group + break GROUP_DROP; + } + } + } + + // champion extra drop + if (victim.isChampion()) + { + if ((victim.getLevel() < killer.getLevel()) && (Rnd.get(100) < Config.CHAMPION_REWARD_LOWER_LEVEL_ITEM_CHANCE)) + { + return calculatedDrops; + } + if ((victim.getLevel() > killer.getLevel()) && (Rnd.get(100) < Config.CHAMPION_REWARD_HIGHER_LEVEL_ITEM_CHANCE)) + { + return calculatedDrops; + } + + // create list + if (calculatedDrops == null) + { + calculatedDrops = new ArrayList<>(); + } + + if (!calculatedDrops.containsAll(Config.CHAMPION_REWARD_ITEMS)) + { + calculatedDrops.addAll(Config.CHAMPION_REWARD_ITEMS); + } + } + + return calculatedDrops; + } + + private List calculateUngroupedDrops(DropType dropType, Creature victim, Creature killer) + { + final List dropList = dropType == DropType.SPOIL ? _dropListSpoil : _dropListDeath; // level difference calculations final int levelDifference = victim.getLevel() - killer.getLevel(); @@ -721,7 +928,7 @@ public class NpcTemplate extends CreatureTemplate implements IIdentifiable } // calculate chances - final ItemHolder drop = calculateDrop(dropItem, victim, killer); + final ItemHolder drop = calculateUngroupedDrop(dropItem, victim, killer); if (drop == null) { continue; @@ -776,20 +983,82 @@ public class NpcTemplate extends CreatureTemplate implements IIdentifiable calculatedDrops = new ArrayList<>(); } - calculatedDrops.addAll(Config.CHAMPION_REWARD_ITEMS); + if (!calculatedDrops.containsAll(Config.CHAMPION_REWARD_ITEMS)) + { + calculatedDrops.addAll(Config.CHAMPION_REWARD_ITEMS); + } } return calculatedDrops; } /** - * All item drop chance calculations are done by this method. * @param dropItem * @param victim * @param killer * @return ItemHolder */ - private ItemHolder calculateDrop(DropHolder dropItem, Creature victim, Creature killer) + private ItemHolder calculateGroupDrop(DropHolder dropItem, Creature victim, Creature killer) + { + final int itemId = dropItem.getItemId(); + final ItemTemplate item = ItemTable.getInstance().getTemplate(itemId); + final boolean champion = victim.isChampion(); + + // calculate amount + double rateAmount = 1; + if (Config.RATE_DROP_AMOUNT_BY_ID.get(itemId) != null) + { + rateAmount *= Config.RATE_DROP_AMOUNT_BY_ID.get(itemId); + if (champion && (itemId == Inventory.ADENA_ID)) + { + rateAmount *= Config.CHAMPION_ADENAS_REWARDS_AMOUNT; + } + } + else if (item.hasExImmediateEffect()) + { + rateAmount *= Config.RATE_HERB_DROP_AMOUNT_MULTIPLIER; + } + else if (victim.isRaid()) + { + rateAmount *= Config.RATE_RAID_DROP_AMOUNT_MULTIPLIER; + } + else + { + rateAmount *= Config.RATE_DEATH_DROP_AMOUNT_MULTIPLIER * (champion ? Config.CHAMPION_REWARDS_AMOUNT : 1); + } + + // premium amount + if (Config.PREMIUM_SYSTEM_ENABLED && (killer.getActingPlayer() != null) && killer.getActingPlayer().hasPremiumStatus()) + { + if (Config.PREMIUM_RATE_DROP_AMOUNT_BY_ID.get(itemId) != null) + { + rateAmount *= Config.PREMIUM_RATE_DROP_AMOUNT_BY_ID.get(itemId); + } + else if (item.hasExImmediateEffect()) + { + // TODO: Premium herb amount? :) + } + else if (victim.isRaid()) + { + // TODO: Premium raid amount? :) + } + else + { + rateAmount *= Config.PREMIUM_RATE_DROP_AMOUNT; + } + } + + // finally + return new ItemHolder(itemId, (long) (Rnd.get(dropItem.getMin(), dropItem.getMax()) * rateAmount)); + } + + /** + * @param dropItem + * @param victim + * @param killer + * @return ItemHolder + */ + private ItemHolder calculateUngroupedDrop(DropHolder dropItem, Creature victim, Creature killer) { switch (dropItem.getDropType()) { diff --git a/L2J_Mobius_05.0_Salvation/java/org/l2jmobius/gameserver/model/holders/DropGroupHolder.java b/L2J_Mobius_05.0_Salvation/java/org/l2jmobius/gameserver/model/holders/DropGroupHolder.java new file mode 100644 index 0000000000..6c2401c0b0 --- /dev/null +++ b/L2J_Mobius_05.0_Salvation/java/org/l2jmobius/gameserver/model/holders/DropGroupHolder.java @@ -0,0 +1,49 @@ +/* + * This file is part of the L2J Mobius project. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package org.l2jmobius.gameserver.model.holders; + +import java.util.ArrayList; +import java.util.List; + +/** + * @author Mobius + */ +public class DropGroupHolder +{ + private final List _dropList = new ArrayList<>(); + private final double _chance; + + public DropGroupHolder(double chance) + { + _chance = chance; + } + + public List getDropList() + { + return _dropList; + } + + public void addDrop(DropHolder holder) + { + _dropList.add(holder); + } + + public double getChance() + { + return _chance; + } +} diff --git a/L2J_Mobius_05.5_EtinasFate/dist/game/data/scripts/handlers/bypasshandlers/NpcViewMod.java b/L2J_Mobius_05.5_EtinasFate/dist/game/data/scripts/handlers/bypasshandlers/NpcViewMod.java index 35d31315ad..fab79abca8 100644 --- a/L2J_Mobius_05.5_EtinasFate/dist/game/data/scripts/handlers/bypasshandlers/NpcViewMod.java +++ b/L2J_Mobius_05.5_EtinasFate/dist/game/data/scripts/handlers/bypasshandlers/NpcViewMod.java @@ -37,6 +37,7 @@ import org.l2jmobius.gameserver.model.actor.Attackable; import org.l2jmobius.gameserver.model.actor.Creature; import org.l2jmobius.gameserver.model.actor.Npc; import org.l2jmobius.gameserver.model.actor.Player; +import org.l2jmobius.gameserver.model.holders.DropGroupHolder; import org.l2jmobius.gameserver.model.holders.DropHolder; import org.l2jmobius.gameserver.model.item.ItemTemplate; import org.l2jmobius.gameserver.model.itemcontainer.Inventory; @@ -326,12 +327,13 @@ public class NpcViewMod implements IBypassHandler private static String getDropListButtons(Npc npc) { final StringBuilder sb = new StringBuilder(); + final List dropListGroups = npc.getTemplate().getDropGroups(); final List dropListDeath = npc.getTemplate().getDropList(); final List dropListSpoil = npc.getTemplate().getSpoilList(); - if ((dropListDeath != null) || (dropListSpoil != null)) + if ((dropListGroups != null) || (dropListDeath != null) || (dropListSpoil != null)) { sb.append("
"); - if (dropListDeath != null) + if ((dropListGroups != null) || (dropListDeath != null)) { sb.append(""); } @@ -348,13 +350,40 @@ public class NpcViewMod implements IBypassHandler private void sendNpcDropList(Player player, Npc npc, DropType dropType, int pageValue) { - final List templateList = dropType == DropType.SPOIL ? npc.getTemplate().getSpoilList() : npc.getTemplate().getDropList(); - if (templateList == null) + List dropList = null; + if (dropType == DropType.SPOIL) + { + dropList = new ArrayList<>(npc.getTemplate().getSpoilList()); + } + else + { + final List drops = npc.getTemplate().getDropList(); + if (drops != null) + { + dropList = new ArrayList<>(drops); + } + final List dropGroups = npc.getTemplate().getDropGroups(); + if (dropGroups != null) + { + if (dropList == null) + { + dropList = new ArrayList<>(); + } + for (DropGroupHolder dropGroup : dropGroups) + { + final double chance = dropGroup.getChance() / 100; + for (DropHolder dropHolder : dropGroup.getDropList()) + { + dropList.add(new DropHolder(dropHolder.getDropType(), dropHolder.getItemId(), dropHolder.getMin(), dropHolder.getMax(), dropHolder.getChance() * chance)); + } + } + } + } + if (dropList == null) { return; } - final List dropList = new ArrayList<>(templateList); Collections.sort(dropList, (d1, d2) -> Integer.valueOf(d1.getItemId()).compareTo(Integer.valueOf(d2.getItemId()))); int pages = dropList.size() / DROP_LIST_ITEMS_PER_PAGE; diff --git a/L2J_Mobius_05.5_EtinasFate/dist/game/data/scripts/handlers/communityboard/DropSearchBoard.java b/L2J_Mobius_05.5_EtinasFate/dist/game/data/scripts/handlers/communityboard/DropSearchBoard.java index 647d14cec5..be1d97660b 100644 --- a/L2J_Mobius_05.5_EtinasFate/dist/game/data/scripts/handlers/communityboard/DropSearchBoard.java +++ b/L2J_Mobius_05.5_EtinasFate/dist/game/data/scripts/handlers/communityboard/DropSearchBoard.java @@ -36,6 +36,7 @@ import org.l2jmobius.gameserver.handler.CommunityBoardHandler; import org.l2jmobius.gameserver.handler.IParseBoardHandler; import org.l2jmobius.gameserver.model.actor.Player; import org.l2jmobius.gameserver.model.actor.templates.NpcTemplate; +import org.l2jmobius.gameserver.model.holders.DropGroupHolder; import org.l2jmobius.gameserver.model.holders.DropHolder; import org.l2jmobius.gameserver.model.item.ItemTemplate; import org.l2jmobius.gameserver.model.itemcontainer.Inventory; @@ -104,6 +105,17 @@ public class DropSearchBoard implements IParseBoardHandler private void buildDropIndex() { + NpcData.getInstance().getTemplates(npc -> npc.getDropGroups() != null).forEach(npcTemplate -> + { + for (DropGroupHolder dropGroup : npcTemplate.getDropGroups()) + { + final double chance = dropGroup.getChance() / 100; + for (DropHolder dropHolder : dropGroup.getDropList()) + { + addToDropList(npcTemplate, new DropHolder(dropHolder.getDropType(), dropHolder.getItemId(), dropHolder.getMin(), dropHolder.getMax(), dropHolder.getChance() * chance)); + } + } + }); NpcData.getInstance().getTemplates(npc -> npc.getDropList() != null).forEach(npcTemplate -> { for (DropHolder dropHolder : npcTemplate.getDropList()) diff --git a/L2J_Mobius_05.5_EtinasFate/dist/game/data/xsd/npcs.xsd b/L2J_Mobius_05.5_EtinasFate/dist/game/data/xsd/npcs.xsd index 117ca4cc1d..2c6e583383 100644 --- a/L2J_Mobius_05.5_EtinasFate/dist/game/data/xsd/npcs.xsd +++ b/L2J_Mobius_05.5_EtinasFate/dist/game/data/xsd/npcs.xsd @@ -1,16 +1,5 @@ - - - - - - - - - - - @@ -260,11 +249,81 @@ - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/L2J_Mobius_05.5_EtinasFate/java/org/l2jmobius/gameserver/data/xml/NpcData.java b/L2J_Mobius_05.5_EtinasFate/java/org/l2jmobius/gameserver/data/xml/NpcData.java index 28850ef6bb..d6d5210168 100644 --- a/L2J_Mobius_05.5_EtinasFate/java/org/l2jmobius/gameserver/data/xml/NpcData.java +++ b/L2J_Mobius_05.5_EtinasFate/java/org/l2jmobius/gameserver/data/xml/NpcData.java @@ -46,6 +46,7 @@ import org.l2jmobius.gameserver.enums.MpRewardType; import org.l2jmobius.gameserver.model.StatSet; import org.l2jmobius.gameserver.model.actor.templates.NpcTemplate; import org.l2jmobius.gameserver.model.effects.EffectType; +import org.l2jmobius.gameserver.model.holders.DropGroupHolder; import org.l2jmobius.gameserver.model.holders.DropHolder; import org.l2jmobius.gameserver.model.skill.Skill; @@ -100,6 +101,7 @@ public class NpcData implements IXmlReader Set clans = null; Set ignoreClanNpcIds = null; List dropLists = null; + List dropGroups = null; set.set("id", npcId); set.set("displayId", parseInteger(attrs, "displayId")); set.set("level", parseByte(attrs, "level")); @@ -436,24 +438,54 @@ public class NpcData implements IXmlReader if (dropType != null) { - if (dropLists == null) - { - dropLists = new ArrayList<>(); - } - for (Node dropNode = dropListsNode.getFirstChild(); dropNode != null; dropNode = dropNode.getNextSibling()) { - final NamedNodeMap dropAttrs = dropNode.getAttributes(); - if ("item".equalsIgnoreCase(dropNode.getNodeName())) + final String nodeName = dropNode.getNodeName(); + if (nodeName.equalsIgnoreCase("group")) { - final DropHolder dropItem = new DropHolder(dropType, parseInteger(dropAttrs, "id"), parseLong(dropAttrs, "min"), parseLong(dropAttrs, "max"), parseDouble(dropAttrs, "chance")); - if (ItemTable.getInstance().getTemplate(parseInteger(dropAttrs, "id")) == null) + if (dropGroups == null) { - LOGGER.warning("DropListItem: Could not find item with id " + parseInteger(dropAttrs, "id") + "."); + dropGroups = new ArrayList<>(); + } + + final DropGroupHolder group = new DropGroupHolder(parseDouble(dropNode.getAttributes(), "chance")); + for (Node groupNode = dropNode.getFirstChild(); groupNode != null; groupNode = groupNode.getNextSibling()) + { + if (groupNode.getNodeName().equalsIgnoreCase("item")) + { + final NamedNodeMap groupAttrs = groupNode.getAttributes(); + final int itemId = parseInteger(groupAttrs, "id"); + + if (ItemTable.getInstance().getTemplate(itemId) == null) + { + LOGGER.warning("DropListItem: Could not find item with id " + itemId + "."); + } + else + { + group.addDrop(new DropHolder(dropType, itemId, parseLong(groupAttrs, "min"), parseLong(groupAttrs, "max"), parseDouble(groupAttrs, "chance"))); + } + } + } + + dropGroups.add(group); + } + else if (nodeName.equalsIgnoreCase("item")) + { + if (dropLists == null) + { + dropLists = new ArrayList<>(); + } + + final NamedNodeMap dropAttrs = dropNode.getAttributes(); + final int itemId = parseInteger(dropAttrs, "id"); + + if (ItemTable.getInstance().getTemplate(itemId) == null) + { + LOGGER.warning("DropListItem: Could not find item with id " + itemId + "."); } else { - dropLists.add(dropItem); + dropLists.add(new DropHolder(dropType, itemId, parseLong(dropAttrs, "min"), parseLong(dropAttrs, "max"), parseDouble(dropAttrs, "chance"))); } } } @@ -612,10 +644,21 @@ public class NpcData implements IXmlReader template.setClans(clans); template.setIgnoreClanNpcIds(ignoreClanNpcIds); + // Clean old drop groups. + template.removeDropGroups(); + + // Set new drop groups. + if (dropGroups != null) + { + template.setDropGroups(dropGroups); + } + + // Clean old drop lists. + template.removeDrops(); + + // Set new drop lists. if (dropLists != null) { - template.removeDrops(); - // Drops are sorted by chance (high to low). Collections.sort(dropLists, (d1, d2) -> Double.valueOf(d2.getChance()).compareTo(Double.valueOf(d1.getChance()))); for (DropHolder dropHolder : dropLists) diff --git a/L2J_Mobius_05.5_EtinasFate/java/org/l2jmobius/gameserver/model/actor/templates/NpcTemplate.java b/L2J_Mobius_05.5_EtinasFate/java/org/l2jmobius/gameserver/model/actor/templates/NpcTemplate.java index 3e40f76887..0c282ce418 100644 --- a/L2J_Mobius_05.5_EtinasFate/java/org/l2jmobius/gameserver/model/actor/templates/NpcTemplate.java +++ b/L2J_Mobius_05.5_EtinasFate/java/org/l2jmobius/gameserver/model/actor/templates/NpcTemplate.java @@ -36,6 +36,7 @@ import org.l2jmobius.gameserver.enums.Race; import org.l2jmobius.gameserver.enums.Sex; import org.l2jmobius.gameserver.model.StatSet; import org.l2jmobius.gameserver.model.actor.Creature; +import org.l2jmobius.gameserver.model.holders.DropGroupHolder; import org.l2jmobius.gameserver.model.holders.DropHolder; import org.l2jmobius.gameserver.model.holders.ItemHolder; import org.l2jmobius.gameserver.model.interfaces.IIdentifiable; @@ -107,6 +108,7 @@ public class NpcTemplate extends CreatureTemplate implements IIdentifiable private Map> _aiSkillLists; private Set _clans; private Set _ignoreClanNpcIds; + private List _dropGroups; private List _dropListDeath; private List _dropListSpoil; private float _collisionRadiusGrown; @@ -649,12 +651,22 @@ public class NpcTemplate extends CreatureTemplate implements IIdentifiable _ignoreClanNpcIds = ignoreClanNpcIds != null ? Collections.unmodifiableSet(ignoreClanNpcIds) : null; } + public void removeDropGroups() + { + _dropGroups = null; + } + public void removeDrops() { _dropListDeath = null; _dropListSpoil = null; } + public void setDropGroups(List groups) + { + _dropGroups = groups; + } + public void addDrop(DropHolder dropHolder) { if (_dropListDeath == null) @@ -673,6 +685,11 @@ public class NpcTemplate extends CreatureTemplate implements IIdentifiable _dropListSpoil.add(dropHolder); } + public List getDropGroups() + { + return _dropGroups; + } + public List getDropList() { return _dropListDeath; @@ -685,11 +702,201 @@ public class NpcTemplate extends CreatureTemplate implements IIdentifiable public List calculateDrops(DropType dropType, Creature victim, Creature killer) { - final List dropList = dropType == DropType.SPOIL ? _dropListSpoil : _dropListDeath; - if (dropList == null) + if (dropType == DropType.DROP) { - return null; + // calculate group drops + List groupDrops = null; + if (_dropGroups != null) + { + groupDrops = calculateGroupDrops(victim, killer); + } + + // calculate ungrouped drops + List ungroupedDrops = null; + if (_dropListDeath != null) + { + ungroupedDrops = calculateUngroupedDrops(dropType, victim, killer); + } + + // return results + if ((groupDrops != null) && (ungroupedDrops != null)) + { + groupDrops.addAll(ungroupedDrops); + ungroupedDrops.clear(); + return groupDrops; + } + if (groupDrops != null) + { + return groupDrops; + } + if (ungroupedDrops != null) + { + return ungroupedDrops; + } } + else if ((dropType == DropType.SPOIL) && (_dropListSpoil != null)) + { + return calculateUngroupedDrops(dropType, victim, killer); + } + + // no drops + return null; + } + + private List calculateGroupDrops(Creature victim, Creature killer) + { + // level difference calculations + final int levelDifference = victim.getLevel() - killer.getLevel(); + final double levelGapChanceToDropAdena = Util.map(levelDifference, -Config.DROP_ADENA_MAX_LEVEL_DIFFERENCE, -Config.DROP_ADENA_MIN_LEVEL_DIFFERENCE, Config.DROP_ADENA_MIN_LEVEL_GAP_CHANCE, 100d); + final double levelGapChanceToDrop = Util.map(levelDifference, -Config.DROP_ITEM_MAX_LEVEL_DIFFERENCE, -Config.DROP_ITEM_MIN_LEVEL_DIFFERENCE, Config.DROP_ITEM_MIN_LEVEL_GAP_CHANCE, 100d); + + int dropOccurrenceCounter = victim.isRaid() ? Config.DROP_MAX_OCCURRENCES_RAIDBOSS : Config.DROP_MAX_OCCURRENCES_NORMAL; + List calculatedDrops = null; + for (DropGroupHolder group : _dropGroups) + { + if (dropOccurrenceCounter <= 0) + { + break; + } + + double groupRate = 1; + final List groupDrops = group.getDropList(); + for (DropHolder dropItem : groupDrops) + { + final int itemId = dropItem.getItemId(); + final ItemTemplate item = ItemTable.getInstance().getTemplate(itemId); + final boolean champion = victim.isChampion(); + + // chance + double rateChance = 1; + final Float itemChance = Config.RATE_DROP_CHANCE_BY_ID.get(itemId); + if (itemChance != null) + { + if (itemChance == 0) + { + continue; + } + + rateChance *= itemChance; + if (champion && (itemId == Inventory.ADENA_ID)) + { + rateChance *= Config.CHAMPION_ADENAS_REWARDS_CHANCE; + } + } + else if (item.hasExImmediateEffect()) + { + rateChance *= Config.RATE_HERB_DROP_CHANCE_MULTIPLIER; + } + else if (victim.isRaid()) + { + rateChance *= Config.RATE_RAID_DROP_CHANCE_MULTIPLIER; + } + else + { + rateChance *= Config.RATE_DEATH_DROP_CHANCE_MULTIPLIER * (champion ? Config.CHAMPION_REWARDS_CHANCE : 1); + } + + // premium chance + if (Config.PREMIUM_SYSTEM_ENABLED && (killer.getActingPlayer() != null) && killer.getActingPlayer().hasPremiumStatus()) + { + if (Config.PREMIUM_RATE_DROP_CHANCE_BY_ID.get(itemId) != null) + { + rateChance *= Config.PREMIUM_RATE_DROP_CHANCE_BY_ID.get(itemId); + } + else if (item.hasExImmediateEffect()) + { + // TODO: Premium herb chance? :) + } + else if (victim.isRaid()) + { + // TODO: Premium raid chance? :) + } + else + { + rateChance *= Config.PREMIUM_RATE_DROP_CHANCE; + } + } + + // keep lowest to avoid chance by id configuration conflicts + groupRate = Math.min(groupRate, rateChance); + } + + if ((Rnd.nextDouble() * 100) < (group.getChance() * groupRate)) + { + double totalChance = 0; // total group chance is 100 + final double dropChance = Rnd.nextDouble() * 100; + GROUP_DROP: for (DropHolder dropItem : groupDrops) + { + if (dropOccurrenceCounter <= 0) + { + break; + } + + // calculate if item will drop + totalChance += dropItem.getChance(); + if (dropChance >= totalChance) + { + continue; + } + + // check level gap that may prevent to drop item + if ((Rnd.nextDouble() * 100) > (dropItem.getItemId() == Inventory.ADENA_ID ? levelGapChanceToDropAdena : levelGapChanceToDrop)) + { + continue; + } + + // create the drop + final ItemHolder drop = calculateGroupDrop(dropItem, victim, killer); + + // create list + if (calculatedDrops == null) + { + calculatedDrops = new ArrayList<>(dropOccurrenceCounter); + } + + // finally + if (group.getChance() < 100) + { + dropOccurrenceCounter--; + } + calculatedDrops.add(drop); + + // no more drops from this group + break GROUP_DROP; + } + } + } + + // champion extra drop + if (victim.isChampion()) + { + if ((victim.getLevel() < killer.getLevel()) && (Rnd.get(100) < Config.CHAMPION_REWARD_LOWER_LEVEL_ITEM_CHANCE)) + { + return calculatedDrops; + } + if ((victim.getLevel() > killer.getLevel()) && (Rnd.get(100) < Config.CHAMPION_REWARD_HIGHER_LEVEL_ITEM_CHANCE)) + { + return calculatedDrops; + } + + // create list + if (calculatedDrops == null) + { + calculatedDrops = new ArrayList<>(); + } + + if (!calculatedDrops.containsAll(Config.CHAMPION_REWARD_ITEMS)) + { + calculatedDrops.addAll(Config.CHAMPION_REWARD_ITEMS); + } + } + + return calculatedDrops; + } + + private List calculateUngroupedDrops(DropType dropType, Creature victim, Creature killer) + { + final List dropList = dropType == DropType.SPOIL ? _dropListSpoil : _dropListDeath; // level difference calculations final int levelDifference = victim.getLevel() - killer.getLevel(); @@ -721,7 +928,7 @@ public class NpcTemplate extends CreatureTemplate implements IIdentifiable } // calculate chances - final ItemHolder drop = calculateDrop(dropItem, victim, killer); + final ItemHolder drop = calculateUngroupedDrop(dropItem, victim, killer); if (drop == null) { continue; @@ -776,20 +983,82 @@ public class NpcTemplate extends CreatureTemplate implements IIdentifiable calculatedDrops = new ArrayList<>(); } - calculatedDrops.addAll(Config.CHAMPION_REWARD_ITEMS); + if (!calculatedDrops.containsAll(Config.CHAMPION_REWARD_ITEMS)) + { + calculatedDrops.addAll(Config.CHAMPION_REWARD_ITEMS); + } } return calculatedDrops; } /** - * All item drop chance calculations are done by this method. * @param dropItem * @param victim * @param killer * @return ItemHolder */ - private ItemHolder calculateDrop(DropHolder dropItem, Creature victim, Creature killer) + private ItemHolder calculateGroupDrop(DropHolder dropItem, Creature victim, Creature killer) + { + final int itemId = dropItem.getItemId(); + final ItemTemplate item = ItemTable.getInstance().getTemplate(itemId); + final boolean champion = victim.isChampion(); + + // calculate amount + double rateAmount = 1; + if (Config.RATE_DROP_AMOUNT_BY_ID.get(itemId) != null) + { + rateAmount *= Config.RATE_DROP_AMOUNT_BY_ID.get(itemId); + if (champion && (itemId == Inventory.ADENA_ID)) + { + rateAmount *= Config.CHAMPION_ADENAS_REWARDS_AMOUNT; + } + } + else if (item.hasExImmediateEffect()) + { + rateAmount *= Config.RATE_HERB_DROP_AMOUNT_MULTIPLIER; + } + else if (victim.isRaid()) + { + rateAmount *= Config.RATE_RAID_DROP_AMOUNT_MULTIPLIER; + } + else + { + rateAmount *= Config.RATE_DEATH_DROP_AMOUNT_MULTIPLIER * (champion ? Config.CHAMPION_REWARDS_AMOUNT : 1); + } + + // premium amount + if (Config.PREMIUM_SYSTEM_ENABLED && (killer.getActingPlayer() != null) && killer.getActingPlayer().hasPremiumStatus()) + { + if (Config.PREMIUM_RATE_DROP_AMOUNT_BY_ID.get(itemId) != null) + { + rateAmount *= Config.PREMIUM_RATE_DROP_AMOUNT_BY_ID.get(itemId); + } + else if (item.hasExImmediateEffect()) + { + // TODO: Premium herb amount? :) + } + else if (victim.isRaid()) + { + // TODO: Premium raid amount? :) + } + else + { + rateAmount *= Config.PREMIUM_RATE_DROP_AMOUNT; + } + } + + // finally + return new ItemHolder(itemId, (long) (Rnd.get(dropItem.getMin(), dropItem.getMax()) * rateAmount)); + } + + /** + * @param dropItem + * @param victim + * @param killer + * @return ItemHolder + */ + private ItemHolder calculateUngroupedDrop(DropHolder dropItem, Creature victim, Creature killer) { switch (dropItem.getDropType()) { diff --git a/L2J_Mobius_05.5_EtinasFate/java/org/l2jmobius/gameserver/model/holders/DropGroupHolder.java b/L2J_Mobius_05.5_EtinasFate/java/org/l2jmobius/gameserver/model/holders/DropGroupHolder.java new file mode 100644 index 0000000000..6c2401c0b0 --- /dev/null +++ b/L2J_Mobius_05.5_EtinasFate/java/org/l2jmobius/gameserver/model/holders/DropGroupHolder.java @@ -0,0 +1,49 @@ +/* + * This file is part of the L2J Mobius project. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package org.l2jmobius.gameserver.model.holders; + +import java.util.ArrayList; +import java.util.List; + +/** + * @author Mobius + */ +public class DropGroupHolder +{ + private final List _dropList = new ArrayList<>(); + private final double _chance; + + public DropGroupHolder(double chance) + { + _chance = chance; + } + + public List getDropList() + { + return _dropList; + } + + public void addDrop(DropHolder holder) + { + _dropList.add(holder); + } + + public double getChance() + { + return _chance; + } +} diff --git a/L2J_Mobius_06.0_Fafurion/dist/game/data/scripts/handlers/bypasshandlers/NpcViewMod.java b/L2J_Mobius_06.0_Fafurion/dist/game/data/scripts/handlers/bypasshandlers/NpcViewMod.java index 35d31315ad..fab79abca8 100644 --- a/L2J_Mobius_06.0_Fafurion/dist/game/data/scripts/handlers/bypasshandlers/NpcViewMod.java +++ b/L2J_Mobius_06.0_Fafurion/dist/game/data/scripts/handlers/bypasshandlers/NpcViewMod.java @@ -37,6 +37,7 @@ import org.l2jmobius.gameserver.model.actor.Attackable; import org.l2jmobius.gameserver.model.actor.Creature; import org.l2jmobius.gameserver.model.actor.Npc; import org.l2jmobius.gameserver.model.actor.Player; +import org.l2jmobius.gameserver.model.holders.DropGroupHolder; import org.l2jmobius.gameserver.model.holders.DropHolder; import org.l2jmobius.gameserver.model.item.ItemTemplate; import org.l2jmobius.gameserver.model.itemcontainer.Inventory; @@ -326,12 +327,13 @@ public class NpcViewMod implements IBypassHandler private static String getDropListButtons(Npc npc) { final StringBuilder sb = new StringBuilder(); + final List dropListGroups = npc.getTemplate().getDropGroups(); final List dropListDeath = npc.getTemplate().getDropList(); final List dropListSpoil = npc.getTemplate().getSpoilList(); - if ((dropListDeath != null) || (dropListSpoil != null)) + if ((dropListGroups != null) || (dropListDeath != null) || (dropListSpoil != null)) { sb.append("
"); - if (dropListDeath != null) + if ((dropListGroups != null) || (dropListDeath != null)) { sb.append(""); } @@ -348,13 +350,40 @@ public class NpcViewMod implements IBypassHandler private void sendNpcDropList(Player player, Npc npc, DropType dropType, int pageValue) { - final List templateList = dropType == DropType.SPOIL ? npc.getTemplate().getSpoilList() : npc.getTemplate().getDropList(); - if (templateList == null) + List dropList = null; + if (dropType == DropType.SPOIL) + { + dropList = new ArrayList<>(npc.getTemplate().getSpoilList()); + } + else + { + final List drops = npc.getTemplate().getDropList(); + if (drops != null) + { + dropList = new ArrayList<>(drops); + } + final List dropGroups = npc.getTemplate().getDropGroups(); + if (dropGroups != null) + { + if (dropList == null) + { + dropList = new ArrayList<>(); + } + for (DropGroupHolder dropGroup : dropGroups) + { + final double chance = dropGroup.getChance() / 100; + for (DropHolder dropHolder : dropGroup.getDropList()) + { + dropList.add(new DropHolder(dropHolder.getDropType(), dropHolder.getItemId(), dropHolder.getMin(), dropHolder.getMax(), dropHolder.getChance() * chance)); + } + } + } + } + if (dropList == null) { return; } - final List dropList = new ArrayList<>(templateList); Collections.sort(dropList, (d1, d2) -> Integer.valueOf(d1.getItemId()).compareTo(Integer.valueOf(d2.getItemId()))); int pages = dropList.size() / DROP_LIST_ITEMS_PER_PAGE; diff --git a/L2J_Mobius_06.0_Fafurion/dist/game/data/scripts/handlers/communityboard/DropSearchBoard.java b/L2J_Mobius_06.0_Fafurion/dist/game/data/scripts/handlers/communityboard/DropSearchBoard.java index 647d14cec5..be1d97660b 100644 --- a/L2J_Mobius_06.0_Fafurion/dist/game/data/scripts/handlers/communityboard/DropSearchBoard.java +++ b/L2J_Mobius_06.0_Fafurion/dist/game/data/scripts/handlers/communityboard/DropSearchBoard.java @@ -36,6 +36,7 @@ import org.l2jmobius.gameserver.handler.CommunityBoardHandler; import org.l2jmobius.gameserver.handler.IParseBoardHandler; import org.l2jmobius.gameserver.model.actor.Player; import org.l2jmobius.gameserver.model.actor.templates.NpcTemplate; +import org.l2jmobius.gameserver.model.holders.DropGroupHolder; import org.l2jmobius.gameserver.model.holders.DropHolder; import org.l2jmobius.gameserver.model.item.ItemTemplate; import org.l2jmobius.gameserver.model.itemcontainer.Inventory; @@ -104,6 +105,17 @@ public class DropSearchBoard implements IParseBoardHandler private void buildDropIndex() { + NpcData.getInstance().getTemplates(npc -> npc.getDropGroups() != null).forEach(npcTemplate -> + { + for (DropGroupHolder dropGroup : npcTemplate.getDropGroups()) + { + final double chance = dropGroup.getChance() / 100; + for (DropHolder dropHolder : dropGroup.getDropList()) + { + addToDropList(npcTemplate, new DropHolder(dropHolder.getDropType(), dropHolder.getItemId(), dropHolder.getMin(), dropHolder.getMax(), dropHolder.getChance() * chance)); + } + } + }); NpcData.getInstance().getTemplates(npc -> npc.getDropList() != null).forEach(npcTemplate -> { for (DropHolder dropHolder : npcTemplate.getDropList()) diff --git a/L2J_Mobius_06.0_Fafurion/dist/game/data/xsd/npcs.xsd b/L2J_Mobius_06.0_Fafurion/dist/game/data/xsd/npcs.xsd index 117ca4cc1d..2c6e583383 100644 --- a/L2J_Mobius_06.0_Fafurion/dist/game/data/xsd/npcs.xsd +++ b/L2J_Mobius_06.0_Fafurion/dist/game/data/xsd/npcs.xsd @@ -1,16 +1,5 @@ - - - - - - - - - - - @@ -260,11 +249,81 @@ - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/L2J_Mobius_06.0_Fafurion/java/org/l2jmobius/gameserver/data/xml/NpcData.java b/L2J_Mobius_06.0_Fafurion/java/org/l2jmobius/gameserver/data/xml/NpcData.java index 28850ef6bb..d6d5210168 100644 --- a/L2J_Mobius_06.0_Fafurion/java/org/l2jmobius/gameserver/data/xml/NpcData.java +++ b/L2J_Mobius_06.0_Fafurion/java/org/l2jmobius/gameserver/data/xml/NpcData.java @@ -46,6 +46,7 @@ import org.l2jmobius.gameserver.enums.MpRewardType; import org.l2jmobius.gameserver.model.StatSet; import org.l2jmobius.gameserver.model.actor.templates.NpcTemplate; import org.l2jmobius.gameserver.model.effects.EffectType; +import org.l2jmobius.gameserver.model.holders.DropGroupHolder; import org.l2jmobius.gameserver.model.holders.DropHolder; import org.l2jmobius.gameserver.model.skill.Skill; @@ -100,6 +101,7 @@ public class NpcData implements IXmlReader Set clans = null; Set ignoreClanNpcIds = null; List dropLists = null; + List dropGroups = null; set.set("id", npcId); set.set("displayId", parseInteger(attrs, "displayId")); set.set("level", parseByte(attrs, "level")); @@ -436,24 +438,54 @@ public class NpcData implements IXmlReader if (dropType != null) { - if (dropLists == null) - { - dropLists = new ArrayList<>(); - } - for (Node dropNode = dropListsNode.getFirstChild(); dropNode != null; dropNode = dropNode.getNextSibling()) { - final NamedNodeMap dropAttrs = dropNode.getAttributes(); - if ("item".equalsIgnoreCase(dropNode.getNodeName())) + final String nodeName = dropNode.getNodeName(); + if (nodeName.equalsIgnoreCase("group")) { - final DropHolder dropItem = new DropHolder(dropType, parseInteger(dropAttrs, "id"), parseLong(dropAttrs, "min"), parseLong(dropAttrs, "max"), parseDouble(dropAttrs, "chance")); - if (ItemTable.getInstance().getTemplate(parseInteger(dropAttrs, "id")) == null) + if (dropGroups == null) { - LOGGER.warning("DropListItem: Could not find item with id " + parseInteger(dropAttrs, "id") + "."); + dropGroups = new ArrayList<>(); + } + + final DropGroupHolder group = new DropGroupHolder(parseDouble(dropNode.getAttributes(), "chance")); + for (Node groupNode = dropNode.getFirstChild(); groupNode != null; groupNode = groupNode.getNextSibling()) + { + if (groupNode.getNodeName().equalsIgnoreCase("item")) + { + final NamedNodeMap groupAttrs = groupNode.getAttributes(); + final int itemId = parseInteger(groupAttrs, "id"); + + if (ItemTable.getInstance().getTemplate(itemId) == null) + { + LOGGER.warning("DropListItem: Could not find item with id " + itemId + "."); + } + else + { + group.addDrop(new DropHolder(dropType, itemId, parseLong(groupAttrs, "min"), parseLong(groupAttrs, "max"), parseDouble(groupAttrs, "chance"))); + } + } + } + + dropGroups.add(group); + } + else if (nodeName.equalsIgnoreCase("item")) + { + if (dropLists == null) + { + dropLists = new ArrayList<>(); + } + + final NamedNodeMap dropAttrs = dropNode.getAttributes(); + final int itemId = parseInteger(dropAttrs, "id"); + + if (ItemTable.getInstance().getTemplate(itemId) == null) + { + LOGGER.warning("DropListItem: Could not find item with id " + itemId + "."); } else { - dropLists.add(dropItem); + dropLists.add(new DropHolder(dropType, itemId, parseLong(dropAttrs, "min"), parseLong(dropAttrs, "max"), parseDouble(dropAttrs, "chance"))); } } } @@ -612,10 +644,21 @@ public class NpcData implements IXmlReader template.setClans(clans); template.setIgnoreClanNpcIds(ignoreClanNpcIds); + // Clean old drop groups. + template.removeDropGroups(); + + // Set new drop groups. + if (dropGroups != null) + { + template.setDropGroups(dropGroups); + } + + // Clean old drop lists. + template.removeDrops(); + + // Set new drop lists. if (dropLists != null) { - template.removeDrops(); - // Drops are sorted by chance (high to low). Collections.sort(dropLists, (d1, d2) -> Double.valueOf(d2.getChance()).compareTo(Double.valueOf(d1.getChance()))); for (DropHolder dropHolder : dropLists) diff --git a/L2J_Mobius_06.0_Fafurion/java/org/l2jmobius/gameserver/model/actor/templates/NpcTemplate.java b/L2J_Mobius_06.0_Fafurion/java/org/l2jmobius/gameserver/model/actor/templates/NpcTemplate.java index 3e40f76887..0c282ce418 100644 --- a/L2J_Mobius_06.0_Fafurion/java/org/l2jmobius/gameserver/model/actor/templates/NpcTemplate.java +++ b/L2J_Mobius_06.0_Fafurion/java/org/l2jmobius/gameserver/model/actor/templates/NpcTemplate.java @@ -36,6 +36,7 @@ import org.l2jmobius.gameserver.enums.Race; import org.l2jmobius.gameserver.enums.Sex; import org.l2jmobius.gameserver.model.StatSet; import org.l2jmobius.gameserver.model.actor.Creature; +import org.l2jmobius.gameserver.model.holders.DropGroupHolder; import org.l2jmobius.gameserver.model.holders.DropHolder; import org.l2jmobius.gameserver.model.holders.ItemHolder; import org.l2jmobius.gameserver.model.interfaces.IIdentifiable; @@ -107,6 +108,7 @@ public class NpcTemplate extends CreatureTemplate implements IIdentifiable private Map> _aiSkillLists; private Set _clans; private Set _ignoreClanNpcIds; + private List _dropGroups; private List _dropListDeath; private List _dropListSpoil; private float _collisionRadiusGrown; @@ -649,12 +651,22 @@ public class NpcTemplate extends CreatureTemplate implements IIdentifiable _ignoreClanNpcIds = ignoreClanNpcIds != null ? Collections.unmodifiableSet(ignoreClanNpcIds) : null; } + public void removeDropGroups() + { + _dropGroups = null; + } + public void removeDrops() { _dropListDeath = null; _dropListSpoil = null; } + public void setDropGroups(List groups) + { + _dropGroups = groups; + } + public void addDrop(DropHolder dropHolder) { if (_dropListDeath == null) @@ -673,6 +685,11 @@ public class NpcTemplate extends CreatureTemplate implements IIdentifiable _dropListSpoil.add(dropHolder); } + public List getDropGroups() + { + return _dropGroups; + } + public List getDropList() { return _dropListDeath; @@ -685,11 +702,201 @@ public class NpcTemplate extends CreatureTemplate implements IIdentifiable public List calculateDrops(DropType dropType, Creature victim, Creature killer) { - final List dropList = dropType == DropType.SPOIL ? _dropListSpoil : _dropListDeath; - if (dropList == null) + if (dropType == DropType.DROP) { - return null; + // calculate group drops + List groupDrops = null; + if (_dropGroups != null) + { + groupDrops = calculateGroupDrops(victim, killer); + } + + // calculate ungrouped drops + List ungroupedDrops = null; + if (_dropListDeath != null) + { + ungroupedDrops = calculateUngroupedDrops(dropType, victim, killer); + } + + // return results + if ((groupDrops != null) && (ungroupedDrops != null)) + { + groupDrops.addAll(ungroupedDrops); + ungroupedDrops.clear(); + return groupDrops; + } + if (groupDrops != null) + { + return groupDrops; + } + if (ungroupedDrops != null) + { + return ungroupedDrops; + } } + else if ((dropType == DropType.SPOIL) && (_dropListSpoil != null)) + { + return calculateUngroupedDrops(dropType, victim, killer); + } + + // no drops + return null; + } + + private List calculateGroupDrops(Creature victim, Creature killer) + { + // level difference calculations + final int levelDifference = victim.getLevel() - killer.getLevel(); + final double levelGapChanceToDropAdena = Util.map(levelDifference, -Config.DROP_ADENA_MAX_LEVEL_DIFFERENCE, -Config.DROP_ADENA_MIN_LEVEL_DIFFERENCE, Config.DROP_ADENA_MIN_LEVEL_GAP_CHANCE, 100d); + final double levelGapChanceToDrop = Util.map(levelDifference, -Config.DROP_ITEM_MAX_LEVEL_DIFFERENCE, -Config.DROP_ITEM_MIN_LEVEL_DIFFERENCE, Config.DROP_ITEM_MIN_LEVEL_GAP_CHANCE, 100d); + + int dropOccurrenceCounter = victim.isRaid() ? Config.DROP_MAX_OCCURRENCES_RAIDBOSS : Config.DROP_MAX_OCCURRENCES_NORMAL; + List calculatedDrops = null; + for (DropGroupHolder group : _dropGroups) + { + if (dropOccurrenceCounter <= 0) + { + break; + } + + double groupRate = 1; + final List groupDrops = group.getDropList(); + for (DropHolder dropItem : groupDrops) + { + final int itemId = dropItem.getItemId(); + final ItemTemplate item = ItemTable.getInstance().getTemplate(itemId); + final boolean champion = victim.isChampion(); + + // chance + double rateChance = 1; + final Float itemChance = Config.RATE_DROP_CHANCE_BY_ID.get(itemId); + if (itemChance != null) + { + if (itemChance == 0) + { + continue; + } + + rateChance *= itemChance; + if (champion && (itemId == Inventory.ADENA_ID)) + { + rateChance *= Config.CHAMPION_ADENAS_REWARDS_CHANCE; + } + } + else if (item.hasExImmediateEffect()) + { + rateChance *= Config.RATE_HERB_DROP_CHANCE_MULTIPLIER; + } + else if (victim.isRaid()) + { + rateChance *= Config.RATE_RAID_DROP_CHANCE_MULTIPLIER; + } + else + { + rateChance *= Config.RATE_DEATH_DROP_CHANCE_MULTIPLIER * (champion ? Config.CHAMPION_REWARDS_CHANCE : 1); + } + + // premium chance + if (Config.PREMIUM_SYSTEM_ENABLED && (killer.getActingPlayer() != null) && killer.getActingPlayer().hasPremiumStatus()) + { + if (Config.PREMIUM_RATE_DROP_CHANCE_BY_ID.get(itemId) != null) + { + rateChance *= Config.PREMIUM_RATE_DROP_CHANCE_BY_ID.get(itemId); + } + else if (item.hasExImmediateEffect()) + { + // TODO: Premium herb chance? :) + } + else if (victim.isRaid()) + { + // TODO: Premium raid chance? :) + } + else + { + rateChance *= Config.PREMIUM_RATE_DROP_CHANCE; + } + } + + // keep lowest to avoid chance by id configuration conflicts + groupRate = Math.min(groupRate, rateChance); + } + + if ((Rnd.nextDouble() * 100) < (group.getChance() * groupRate)) + { + double totalChance = 0; // total group chance is 100 + final double dropChance = Rnd.nextDouble() * 100; + GROUP_DROP: for (DropHolder dropItem : groupDrops) + { + if (dropOccurrenceCounter <= 0) + { + break; + } + + // calculate if item will drop + totalChance += dropItem.getChance(); + if (dropChance >= totalChance) + { + continue; + } + + // check level gap that may prevent to drop item + if ((Rnd.nextDouble() * 100) > (dropItem.getItemId() == Inventory.ADENA_ID ? levelGapChanceToDropAdena : levelGapChanceToDrop)) + { + continue; + } + + // create the drop + final ItemHolder drop = calculateGroupDrop(dropItem, victim, killer); + + // create list + if (calculatedDrops == null) + { + calculatedDrops = new ArrayList<>(dropOccurrenceCounter); + } + + // finally + if (group.getChance() < 100) + { + dropOccurrenceCounter--; + } + calculatedDrops.add(drop); + + // no more drops from this group + break GROUP_DROP; + } + } + } + + // champion extra drop + if (victim.isChampion()) + { + if ((victim.getLevel() < killer.getLevel()) && (Rnd.get(100) < Config.CHAMPION_REWARD_LOWER_LEVEL_ITEM_CHANCE)) + { + return calculatedDrops; + } + if ((victim.getLevel() > killer.getLevel()) && (Rnd.get(100) < Config.CHAMPION_REWARD_HIGHER_LEVEL_ITEM_CHANCE)) + { + return calculatedDrops; + } + + // create list + if (calculatedDrops == null) + { + calculatedDrops = new ArrayList<>(); + } + + if (!calculatedDrops.containsAll(Config.CHAMPION_REWARD_ITEMS)) + { + calculatedDrops.addAll(Config.CHAMPION_REWARD_ITEMS); + } + } + + return calculatedDrops; + } + + private List calculateUngroupedDrops(DropType dropType, Creature victim, Creature killer) + { + final List dropList = dropType == DropType.SPOIL ? _dropListSpoil : _dropListDeath; // level difference calculations final int levelDifference = victim.getLevel() - killer.getLevel(); @@ -721,7 +928,7 @@ public class NpcTemplate extends CreatureTemplate implements IIdentifiable } // calculate chances - final ItemHolder drop = calculateDrop(dropItem, victim, killer); + final ItemHolder drop = calculateUngroupedDrop(dropItem, victim, killer); if (drop == null) { continue; @@ -776,20 +983,82 @@ public class NpcTemplate extends CreatureTemplate implements IIdentifiable calculatedDrops = new ArrayList<>(); } - calculatedDrops.addAll(Config.CHAMPION_REWARD_ITEMS); + if (!calculatedDrops.containsAll(Config.CHAMPION_REWARD_ITEMS)) + { + calculatedDrops.addAll(Config.CHAMPION_REWARD_ITEMS); + } } return calculatedDrops; } /** - * All item drop chance calculations are done by this method. * @param dropItem * @param victim * @param killer * @return ItemHolder */ - private ItemHolder calculateDrop(DropHolder dropItem, Creature victim, Creature killer) + private ItemHolder calculateGroupDrop(DropHolder dropItem, Creature victim, Creature killer) + { + final int itemId = dropItem.getItemId(); + final ItemTemplate item = ItemTable.getInstance().getTemplate(itemId); + final boolean champion = victim.isChampion(); + + // calculate amount + double rateAmount = 1; + if (Config.RATE_DROP_AMOUNT_BY_ID.get(itemId) != null) + { + rateAmount *= Config.RATE_DROP_AMOUNT_BY_ID.get(itemId); + if (champion && (itemId == Inventory.ADENA_ID)) + { + rateAmount *= Config.CHAMPION_ADENAS_REWARDS_AMOUNT; + } + } + else if (item.hasExImmediateEffect()) + { + rateAmount *= Config.RATE_HERB_DROP_AMOUNT_MULTIPLIER; + } + else if (victim.isRaid()) + { + rateAmount *= Config.RATE_RAID_DROP_AMOUNT_MULTIPLIER; + } + else + { + rateAmount *= Config.RATE_DEATH_DROP_AMOUNT_MULTIPLIER * (champion ? Config.CHAMPION_REWARDS_AMOUNT : 1); + } + + // premium amount + if (Config.PREMIUM_SYSTEM_ENABLED && (killer.getActingPlayer() != null) && killer.getActingPlayer().hasPremiumStatus()) + { + if (Config.PREMIUM_RATE_DROP_AMOUNT_BY_ID.get(itemId) != null) + { + rateAmount *= Config.PREMIUM_RATE_DROP_AMOUNT_BY_ID.get(itemId); + } + else if (item.hasExImmediateEffect()) + { + // TODO: Premium herb amount? :) + } + else if (victim.isRaid()) + { + // TODO: Premium raid amount? :) + } + else + { + rateAmount *= Config.PREMIUM_RATE_DROP_AMOUNT; + } + } + + // finally + return new ItemHolder(itemId, (long) (Rnd.get(dropItem.getMin(), dropItem.getMax()) * rateAmount)); + } + + /** + * @param dropItem + * @param victim + * @param killer + * @return ItemHolder + */ + private ItemHolder calculateUngroupedDrop(DropHolder dropItem, Creature victim, Creature killer) { switch (dropItem.getDropType()) { diff --git a/L2J_Mobius_06.0_Fafurion/java/org/l2jmobius/gameserver/model/holders/DropGroupHolder.java b/L2J_Mobius_06.0_Fafurion/java/org/l2jmobius/gameserver/model/holders/DropGroupHolder.java new file mode 100644 index 0000000000..6c2401c0b0 --- /dev/null +++ b/L2J_Mobius_06.0_Fafurion/java/org/l2jmobius/gameserver/model/holders/DropGroupHolder.java @@ -0,0 +1,49 @@ +/* + * This file is part of the L2J Mobius project. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package org.l2jmobius.gameserver.model.holders; + +import java.util.ArrayList; +import java.util.List; + +/** + * @author Mobius + */ +public class DropGroupHolder +{ + private final List _dropList = new ArrayList<>(); + private final double _chance; + + public DropGroupHolder(double chance) + { + _chance = chance; + } + + public List getDropList() + { + return _dropList; + } + + public void addDrop(DropHolder holder) + { + _dropList.add(holder); + } + + public double getChance() + { + return _chance; + } +} diff --git a/L2J_Mobius_07.0_PreludeOfWar/dist/game/data/scripts/handlers/bypasshandlers/NpcViewMod.java b/L2J_Mobius_07.0_PreludeOfWar/dist/game/data/scripts/handlers/bypasshandlers/NpcViewMod.java index 35d31315ad..fab79abca8 100644 --- a/L2J_Mobius_07.0_PreludeOfWar/dist/game/data/scripts/handlers/bypasshandlers/NpcViewMod.java +++ b/L2J_Mobius_07.0_PreludeOfWar/dist/game/data/scripts/handlers/bypasshandlers/NpcViewMod.java @@ -37,6 +37,7 @@ import org.l2jmobius.gameserver.model.actor.Attackable; import org.l2jmobius.gameserver.model.actor.Creature; import org.l2jmobius.gameserver.model.actor.Npc; import org.l2jmobius.gameserver.model.actor.Player; +import org.l2jmobius.gameserver.model.holders.DropGroupHolder; import org.l2jmobius.gameserver.model.holders.DropHolder; import org.l2jmobius.gameserver.model.item.ItemTemplate; import org.l2jmobius.gameserver.model.itemcontainer.Inventory; @@ -326,12 +327,13 @@ public class NpcViewMod implements IBypassHandler private static String getDropListButtons(Npc npc) { final StringBuilder sb = new StringBuilder(); + final List dropListGroups = npc.getTemplate().getDropGroups(); final List dropListDeath = npc.getTemplate().getDropList(); final List dropListSpoil = npc.getTemplate().getSpoilList(); - if ((dropListDeath != null) || (dropListSpoil != null)) + if ((dropListGroups != null) || (dropListDeath != null) || (dropListSpoil != null)) { sb.append("
"); - if (dropListDeath != null) + if ((dropListGroups != null) || (dropListDeath != null)) { sb.append(""); } @@ -348,13 +350,40 @@ public class NpcViewMod implements IBypassHandler private void sendNpcDropList(Player player, Npc npc, DropType dropType, int pageValue) { - final List templateList = dropType == DropType.SPOIL ? npc.getTemplate().getSpoilList() : npc.getTemplate().getDropList(); - if (templateList == null) + List dropList = null; + if (dropType == DropType.SPOIL) + { + dropList = new ArrayList<>(npc.getTemplate().getSpoilList()); + } + else + { + final List drops = npc.getTemplate().getDropList(); + if (drops != null) + { + dropList = new ArrayList<>(drops); + } + final List dropGroups = npc.getTemplate().getDropGroups(); + if (dropGroups != null) + { + if (dropList == null) + { + dropList = new ArrayList<>(); + } + for (DropGroupHolder dropGroup : dropGroups) + { + final double chance = dropGroup.getChance() / 100; + for (DropHolder dropHolder : dropGroup.getDropList()) + { + dropList.add(new DropHolder(dropHolder.getDropType(), dropHolder.getItemId(), dropHolder.getMin(), dropHolder.getMax(), dropHolder.getChance() * chance)); + } + } + } + } + if (dropList == null) { return; } - final List dropList = new ArrayList<>(templateList); Collections.sort(dropList, (d1, d2) -> Integer.valueOf(d1.getItemId()).compareTo(Integer.valueOf(d2.getItemId()))); int pages = dropList.size() / DROP_LIST_ITEMS_PER_PAGE; diff --git a/L2J_Mobius_07.0_PreludeOfWar/dist/game/data/scripts/handlers/communityboard/DropSearchBoard.java b/L2J_Mobius_07.0_PreludeOfWar/dist/game/data/scripts/handlers/communityboard/DropSearchBoard.java index 647d14cec5..be1d97660b 100644 --- a/L2J_Mobius_07.0_PreludeOfWar/dist/game/data/scripts/handlers/communityboard/DropSearchBoard.java +++ b/L2J_Mobius_07.0_PreludeOfWar/dist/game/data/scripts/handlers/communityboard/DropSearchBoard.java @@ -36,6 +36,7 @@ import org.l2jmobius.gameserver.handler.CommunityBoardHandler; import org.l2jmobius.gameserver.handler.IParseBoardHandler; import org.l2jmobius.gameserver.model.actor.Player; import org.l2jmobius.gameserver.model.actor.templates.NpcTemplate; +import org.l2jmobius.gameserver.model.holders.DropGroupHolder; import org.l2jmobius.gameserver.model.holders.DropHolder; import org.l2jmobius.gameserver.model.item.ItemTemplate; import org.l2jmobius.gameserver.model.itemcontainer.Inventory; @@ -104,6 +105,17 @@ public class DropSearchBoard implements IParseBoardHandler private void buildDropIndex() { + NpcData.getInstance().getTemplates(npc -> npc.getDropGroups() != null).forEach(npcTemplate -> + { + for (DropGroupHolder dropGroup : npcTemplate.getDropGroups()) + { + final double chance = dropGroup.getChance() / 100; + for (DropHolder dropHolder : dropGroup.getDropList()) + { + addToDropList(npcTemplate, new DropHolder(dropHolder.getDropType(), dropHolder.getItemId(), dropHolder.getMin(), dropHolder.getMax(), dropHolder.getChance() * chance)); + } + } + }); NpcData.getInstance().getTemplates(npc -> npc.getDropList() != null).forEach(npcTemplate -> { for (DropHolder dropHolder : npcTemplate.getDropList()) diff --git a/L2J_Mobius_07.0_PreludeOfWar/dist/game/data/xsd/npcs.xsd b/L2J_Mobius_07.0_PreludeOfWar/dist/game/data/xsd/npcs.xsd index 117ca4cc1d..2c6e583383 100644 --- a/L2J_Mobius_07.0_PreludeOfWar/dist/game/data/xsd/npcs.xsd +++ b/L2J_Mobius_07.0_PreludeOfWar/dist/game/data/xsd/npcs.xsd @@ -1,16 +1,5 @@ - - - - - - - - - - - @@ -260,11 +249,81 @@ - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/L2J_Mobius_07.0_PreludeOfWar/java/org/l2jmobius/gameserver/data/xml/NpcData.java b/L2J_Mobius_07.0_PreludeOfWar/java/org/l2jmobius/gameserver/data/xml/NpcData.java index 28850ef6bb..d6d5210168 100644 --- a/L2J_Mobius_07.0_PreludeOfWar/java/org/l2jmobius/gameserver/data/xml/NpcData.java +++ b/L2J_Mobius_07.0_PreludeOfWar/java/org/l2jmobius/gameserver/data/xml/NpcData.java @@ -46,6 +46,7 @@ import org.l2jmobius.gameserver.enums.MpRewardType; import org.l2jmobius.gameserver.model.StatSet; import org.l2jmobius.gameserver.model.actor.templates.NpcTemplate; import org.l2jmobius.gameserver.model.effects.EffectType; +import org.l2jmobius.gameserver.model.holders.DropGroupHolder; import org.l2jmobius.gameserver.model.holders.DropHolder; import org.l2jmobius.gameserver.model.skill.Skill; @@ -100,6 +101,7 @@ public class NpcData implements IXmlReader Set clans = null; Set ignoreClanNpcIds = null; List dropLists = null; + List dropGroups = null; set.set("id", npcId); set.set("displayId", parseInteger(attrs, "displayId")); set.set("level", parseByte(attrs, "level")); @@ -436,24 +438,54 @@ public class NpcData implements IXmlReader if (dropType != null) { - if (dropLists == null) - { - dropLists = new ArrayList<>(); - } - for (Node dropNode = dropListsNode.getFirstChild(); dropNode != null; dropNode = dropNode.getNextSibling()) { - final NamedNodeMap dropAttrs = dropNode.getAttributes(); - if ("item".equalsIgnoreCase(dropNode.getNodeName())) + final String nodeName = dropNode.getNodeName(); + if (nodeName.equalsIgnoreCase("group")) { - final DropHolder dropItem = new DropHolder(dropType, parseInteger(dropAttrs, "id"), parseLong(dropAttrs, "min"), parseLong(dropAttrs, "max"), parseDouble(dropAttrs, "chance")); - if (ItemTable.getInstance().getTemplate(parseInteger(dropAttrs, "id")) == null) + if (dropGroups == null) { - LOGGER.warning("DropListItem: Could not find item with id " + parseInteger(dropAttrs, "id") + "."); + dropGroups = new ArrayList<>(); + } + + final DropGroupHolder group = new DropGroupHolder(parseDouble(dropNode.getAttributes(), "chance")); + for (Node groupNode = dropNode.getFirstChild(); groupNode != null; groupNode = groupNode.getNextSibling()) + { + if (groupNode.getNodeName().equalsIgnoreCase("item")) + { + final NamedNodeMap groupAttrs = groupNode.getAttributes(); + final int itemId = parseInteger(groupAttrs, "id"); + + if (ItemTable.getInstance().getTemplate(itemId) == null) + { + LOGGER.warning("DropListItem: Could not find item with id " + itemId + "."); + } + else + { + group.addDrop(new DropHolder(dropType, itemId, parseLong(groupAttrs, "min"), parseLong(groupAttrs, "max"), parseDouble(groupAttrs, "chance"))); + } + } + } + + dropGroups.add(group); + } + else if (nodeName.equalsIgnoreCase("item")) + { + if (dropLists == null) + { + dropLists = new ArrayList<>(); + } + + final NamedNodeMap dropAttrs = dropNode.getAttributes(); + final int itemId = parseInteger(dropAttrs, "id"); + + if (ItemTable.getInstance().getTemplate(itemId) == null) + { + LOGGER.warning("DropListItem: Could not find item with id " + itemId + "."); } else { - dropLists.add(dropItem); + dropLists.add(new DropHolder(dropType, itemId, parseLong(dropAttrs, "min"), parseLong(dropAttrs, "max"), parseDouble(dropAttrs, "chance"))); } } } @@ -612,10 +644,21 @@ public class NpcData implements IXmlReader template.setClans(clans); template.setIgnoreClanNpcIds(ignoreClanNpcIds); + // Clean old drop groups. + template.removeDropGroups(); + + // Set new drop groups. + if (dropGroups != null) + { + template.setDropGroups(dropGroups); + } + + // Clean old drop lists. + template.removeDrops(); + + // Set new drop lists. if (dropLists != null) { - template.removeDrops(); - // Drops are sorted by chance (high to low). Collections.sort(dropLists, (d1, d2) -> Double.valueOf(d2.getChance()).compareTo(Double.valueOf(d1.getChance()))); for (DropHolder dropHolder : dropLists) diff --git a/L2J_Mobius_07.0_PreludeOfWar/java/org/l2jmobius/gameserver/model/actor/templates/NpcTemplate.java b/L2J_Mobius_07.0_PreludeOfWar/java/org/l2jmobius/gameserver/model/actor/templates/NpcTemplate.java index 3e40f76887..0c282ce418 100644 --- a/L2J_Mobius_07.0_PreludeOfWar/java/org/l2jmobius/gameserver/model/actor/templates/NpcTemplate.java +++ b/L2J_Mobius_07.0_PreludeOfWar/java/org/l2jmobius/gameserver/model/actor/templates/NpcTemplate.java @@ -36,6 +36,7 @@ import org.l2jmobius.gameserver.enums.Race; import org.l2jmobius.gameserver.enums.Sex; import org.l2jmobius.gameserver.model.StatSet; import org.l2jmobius.gameserver.model.actor.Creature; +import org.l2jmobius.gameserver.model.holders.DropGroupHolder; import org.l2jmobius.gameserver.model.holders.DropHolder; import org.l2jmobius.gameserver.model.holders.ItemHolder; import org.l2jmobius.gameserver.model.interfaces.IIdentifiable; @@ -107,6 +108,7 @@ public class NpcTemplate extends CreatureTemplate implements IIdentifiable private Map> _aiSkillLists; private Set _clans; private Set _ignoreClanNpcIds; + private List _dropGroups; private List _dropListDeath; private List _dropListSpoil; private float _collisionRadiusGrown; @@ -649,12 +651,22 @@ public class NpcTemplate extends CreatureTemplate implements IIdentifiable _ignoreClanNpcIds = ignoreClanNpcIds != null ? Collections.unmodifiableSet(ignoreClanNpcIds) : null; } + public void removeDropGroups() + { + _dropGroups = null; + } + public void removeDrops() { _dropListDeath = null; _dropListSpoil = null; } + public void setDropGroups(List groups) + { + _dropGroups = groups; + } + public void addDrop(DropHolder dropHolder) { if (_dropListDeath == null) @@ -673,6 +685,11 @@ public class NpcTemplate extends CreatureTemplate implements IIdentifiable _dropListSpoil.add(dropHolder); } + public List getDropGroups() + { + return _dropGroups; + } + public List getDropList() { return _dropListDeath; @@ -685,11 +702,201 @@ public class NpcTemplate extends CreatureTemplate implements IIdentifiable public List calculateDrops(DropType dropType, Creature victim, Creature killer) { - final List dropList = dropType == DropType.SPOIL ? _dropListSpoil : _dropListDeath; - if (dropList == null) + if (dropType == DropType.DROP) { - return null; + // calculate group drops + List groupDrops = null; + if (_dropGroups != null) + { + groupDrops = calculateGroupDrops(victim, killer); + } + + // calculate ungrouped drops + List ungroupedDrops = null; + if (_dropListDeath != null) + { + ungroupedDrops = calculateUngroupedDrops(dropType, victim, killer); + } + + // return results + if ((groupDrops != null) && (ungroupedDrops != null)) + { + groupDrops.addAll(ungroupedDrops); + ungroupedDrops.clear(); + return groupDrops; + } + if (groupDrops != null) + { + return groupDrops; + } + if (ungroupedDrops != null) + { + return ungroupedDrops; + } } + else if ((dropType == DropType.SPOIL) && (_dropListSpoil != null)) + { + return calculateUngroupedDrops(dropType, victim, killer); + } + + // no drops + return null; + } + + private List calculateGroupDrops(Creature victim, Creature killer) + { + // level difference calculations + final int levelDifference = victim.getLevel() - killer.getLevel(); + final double levelGapChanceToDropAdena = Util.map(levelDifference, -Config.DROP_ADENA_MAX_LEVEL_DIFFERENCE, -Config.DROP_ADENA_MIN_LEVEL_DIFFERENCE, Config.DROP_ADENA_MIN_LEVEL_GAP_CHANCE, 100d); + final double levelGapChanceToDrop = Util.map(levelDifference, -Config.DROP_ITEM_MAX_LEVEL_DIFFERENCE, -Config.DROP_ITEM_MIN_LEVEL_DIFFERENCE, Config.DROP_ITEM_MIN_LEVEL_GAP_CHANCE, 100d); + + int dropOccurrenceCounter = victim.isRaid() ? Config.DROP_MAX_OCCURRENCES_RAIDBOSS : Config.DROP_MAX_OCCURRENCES_NORMAL; + List calculatedDrops = null; + for (DropGroupHolder group : _dropGroups) + { + if (dropOccurrenceCounter <= 0) + { + break; + } + + double groupRate = 1; + final List groupDrops = group.getDropList(); + for (DropHolder dropItem : groupDrops) + { + final int itemId = dropItem.getItemId(); + final ItemTemplate item = ItemTable.getInstance().getTemplate(itemId); + final boolean champion = victim.isChampion(); + + // chance + double rateChance = 1; + final Float itemChance = Config.RATE_DROP_CHANCE_BY_ID.get(itemId); + if (itemChance != null) + { + if (itemChance == 0) + { + continue; + } + + rateChance *= itemChance; + if (champion && (itemId == Inventory.ADENA_ID)) + { + rateChance *= Config.CHAMPION_ADENAS_REWARDS_CHANCE; + } + } + else if (item.hasExImmediateEffect()) + { + rateChance *= Config.RATE_HERB_DROP_CHANCE_MULTIPLIER; + } + else if (victim.isRaid()) + { + rateChance *= Config.RATE_RAID_DROP_CHANCE_MULTIPLIER; + } + else + { + rateChance *= Config.RATE_DEATH_DROP_CHANCE_MULTIPLIER * (champion ? Config.CHAMPION_REWARDS_CHANCE : 1); + } + + // premium chance + if (Config.PREMIUM_SYSTEM_ENABLED && (killer.getActingPlayer() != null) && killer.getActingPlayer().hasPremiumStatus()) + { + if (Config.PREMIUM_RATE_DROP_CHANCE_BY_ID.get(itemId) != null) + { + rateChance *= Config.PREMIUM_RATE_DROP_CHANCE_BY_ID.get(itemId); + } + else if (item.hasExImmediateEffect()) + { + // TODO: Premium herb chance? :) + } + else if (victim.isRaid()) + { + // TODO: Premium raid chance? :) + } + else + { + rateChance *= Config.PREMIUM_RATE_DROP_CHANCE; + } + } + + // keep lowest to avoid chance by id configuration conflicts + groupRate = Math.min(groupRate, rateChance); + } + + if ((Rnd.nextDouble() * 100) < (group.getChance() * groupRate)) + { + double totalChance = 0; // total group chance is 100 + final double dropChance = Rnd.nextDouble() * 100; + GROUP_DROP: for (DropHolder dropItem : groupDrops) + { + if (dropOccurrenceCounter <= 0) + { + break; + } + + // calculate if item will drop + totalChance += dropItem.getChance(); + if (dropChance >= totalChance) + { + continue; + } + + // check level gap that may prevent to drop item + if ((Rnd.nextDouble() * 100) > (dropItem.getItemId() == Inventory.ADENA_ID ? levelGapChanceToDropAdena : levelGapChanceToDrop)) + { + continue; + } + + // create the drop + final ItemHolder drop = calculateGroupDrop(dropItem, victim, killer); + + // create list + if (calculatedDrops == null) + { + calculatedDrops = new ArrayList<>(dropOccurrenceCounter); + } + + // finally + if (group.getChance() < 100) + { + dropOccurrenceCounter--; + } + calculatedDrops.add(drop); + + // no more drops from this group + break GROUP_DROP; + } + } + } + + // champion extra drop + if (victim.isChampion()) + { + if ((victim.getLevel() < killer.getLevel()) && (Rnd.get(100) < Config.CHAMPION_REWARD_LOWER_LEVEL_ITEM_CHANCE)) + { + return calculatedDrops; + } + if ((victim.getLevel() > killer.getLevel()) && (Rnd.get(100) < Config.CHAMPION_REWARD_HIGHER_LEVEL_ITEM_CHANCE)) + { + return calculatedDrops; + } + + // create list + if (calculatedDrops == null) + { + calculatedDrops = new ArrayList<>(); + } + + if (!calculatedDrops.containsAll(Config.CHAMPION_REWARD_ITEMS)) + { + calculatedDrops.addAll(Config.CHAMPION_REWARD_ITEMS); + } + } + + return calculatedDrops; + } + + private List calculateUngroupedDrops(DropType dropType, Creature victim, Creature killer) + { + final List dropList = dropType == DropType.SPOIL ? _dropListSpoil : _dropListDeath; // level difference calculations final int levelDifference = victim.getLevel() - killer.getLevel(); @@ -721,7 +928,7 @@ public class NpcTemplate extends CreatureTemplate implements IIdentifiable } // calculate chances - final ItemHolder drop = calculateDrop(dropItem, victim, killer); + final ItemHolder drop = calculateUngroupedDrop(dropItem, victim, killer); if (drop == null) { continue; @@ -776,20 +983,82 @@ public class NpcTemplate extends CreatureTemplate implements IIdentifiable calculatedDrops = new ArrayList<>(); } - calculatedDrops.addAll(Config.CHAMPION_REWARD_ITEMS); + if (!calculatedDrops.containsAll(Config.CHAMPION_REWARD_ITEMS)) + { + calculatedDrops.addAll(Config.CHAMPION_REWARD_ITEMS); + } } return calculatedDrops; } /** - * All item drop chance calculations are done by this method. * @param dropItem * @param victim * @param killer * @return ItemHolder */ - private ItemHolder calculateDrop(DropHolder dropItem, Creature victim, Creature killer) + private ItemHolder calculateGroupDrop(DropHolder dropItem, Creature victim, Creature killer) + { + final int itemId = dropItem.getItemId(); + final ItemTemplate item = ItemTable.getInstance().getTemplate(itemId); + final boolean champion = victim.isChampion(); + + // calculate amount + double rateAmount = 1; + if (Config.RATE_DROP_AMOUNT_BY_ID.get(itemId) != null) + { + rateAmount *= Config.RATE_DROP_AMOUNT_BY_ID.get(itemId); + if (champion && (itemId == Inventory.ADENA_ID)) + { + rateAmount *= Config.CHAMPION_ADENAS_REWARDS_AMOUNT; + } + } + else if (item.hasExImmediateEffect()) + { + rateAmount *= Config.RATE_HERB_DROP_AMOUNT_MULTIPLIER; + } + else if (victim.isRaid()) + { + rateAmount *= Config.RATE_RAID_DROP_AMOUNT_MULTIPLIER; + } + else + { + rateAmount *= Config.RATE_DEATH_DROP_AMOUNT_MULTIPLIER * (champion ? Config.CHAMPION_REWARDS_AMOUNT : 1); + } + + // premium amount + if (Config.PREMIUM_SYSTEM_ENABLED && (killer.getActingPlayer() != null) && killer.getActingPlayer().hasPremiumStatus()) + { + if (Config.PREMIUM_RATE_DROP_AMOUNT_BY_ID.get(itemId) != null) + { + rateAmount *= Config.PREMIUM_RATE_DROP_AMOUNT_BY_ID.get(itemId); + } + else if (item.hasExImmediateEffect()) + { + // TODO: Premium herb amount? :) + } + else if (victim.isRaid()) + { + // TODO: Premium raid amount? :) + } + else + { + rateAmount *= Config.PREMIUM_RATE_DROP_AMOUNT; + } + } + + // finally + return new ItemHolder(itemId, (long) (Rnd.get(dropItem.getMin(), dropItem.getMax()) * rateAmount)); + } + + /** + * @param dropItem + * @param victim + * @param killer + * @return ItemHolder + */ + private ItemHolder calculateUngroupedDrop(DropHolder dropItem, Creature victim, Creature killer) { switch (dropItem.getDropType()) { diff --git a/L2J_Mobius_07.0_PreludeOfWar/java/org/l2jmobius/gameserver/model/holders/DropGroupHolder.java b/L2J_Mobius_07.0_PreludeOfWar/java/org/l2jmobius/gameserver/model/holders/DropGroupHolder.java new file mode 100644 index 0000000000..6c2401c0b0 --- /dev/null +++ b/L2J_Mobius_07.0_PreludeOfWar/java/org/l2jmobius/gameserver/model/holders/DropGroupHolder.java @@ -0,0 +1,49 @@ +/* + * This file is part of the L2J Mobius project. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package org.l2jmobius.gameserver.model.holders; + +import java.util.ArrayList; +import java.util.List; + +/** + * @author Mobius + */ +public class DropGroupHolder +{ + private final List _dropList = new ArrayList<>(); + private final double _chance; + + public DropGroupHolder(double chance) + { + _chance = chance; + } + + public List getDropList() + { + return _dropList; + } + + public void addDrop(DropHolder holder) + { + _dropList.add(holder); + } + + public double getChance() + { + return _chance; + } +} diff --git a/L2J_Mobius_08.2_Homunculus/dist/game/data/scripts/handlers/bypasshandlers/NpcViewMod.java b/L2J_Mobius_08.2_Homunculus/dist/game/data/scripts/handlers/bypasshandlers/NpcViewMod.java index 35d31315ad..fab79abca8 100644 --- a/L2J_Mobius_08.2_Homunculus/dist/game/data/scripts/handlers/bypasshandlers/NpcViewMod.java +++ b/L2J_Mobius_08.2_Homunculus/dist/game/data/scripts/handlers/bypasshandlers/NpcViewMod.java @@ -37,6 +37,7 @@ import org.l2jmobius.gameserver.model.actor.Attackable; import org.l2jmobius.gameserver.model.actor.Creature; import org.l2jmobius.gameserver.model.actor.Npc; import org.l2jmobius.gameserver.model.actor.Player; +import org.l2jmobius.gameserver.model.holders.DropGroupHolder; import org.l2jmobius.gameserver.model.holders.DropHolder; import org.l2jmobius.gameserver.model.item.ItemTemplate; import org.l2jmobius.gameserver.model.itemcontainer.Inventory; @@ -326,12 +327,13 @@ public class NpcViewMod implements IBypassHandler private static String getDropListButtons(Npc npc) { final StringBuilder sb = new StringBuilder(); + final List dropListGroups = npc.getTemplate().getDropGroups(); final List dropListDeath = npc.getTemplate().getDropList(); final List dropListSpoil = npc.getTemplate().getSpoilList(); - if ((dropListDeath != null) || (dropListSpoil != null)) + if ((dropListGroups != null) || (dropListDeath != null) || (dropListSpoil != null)) { sb.append("
"); - if (dropListDeath != null) + if ((dropListGroups != null) || (dropListDeath != null)) { sb.append(""); } @@ -348,13 +350,40 @@ public class NpcViewMod implements IBypassHandler private void sendNpcDropList(Player player, Npc npc, DropType dropType, int pageValue) { - final List templateList = dropType == DropType.SPOIL ? npc.getTemplate().getSpoilList() : npc.getTemplate().getDropList(); - if (templateList == null) + List dropList = null; + if (dropType == DropType.SPOIL) + { + dropList = new ArrayList<>(npc.getTemplate().getSpoilList()); + } + else + { + final List drops = npc.getTemplate().getDropList(); + if (drops != null) + { + dropList = new ArrayList<>(drops); + } + final List dropGroups = npc.getTemplate().getDropGroups(); + if (dropGroups != null) + { + if (dropList == null) + { + dropList = new ArrayList<>(); + } + for (DropGroupHolder dropGroup : dropGroups) + { + final double chance = dropGroup.getChance() / 100; + for (DropHolder dropHolder : dropGroup.getDropList()) + { + dropList.add(new DropHolder(dropHolder.getDropType(), dropHolder.getItemId(), dropHolder.getMin(), dropHolder.getMax(), dropHolder.getChance() * chance)); + } + } + } + } + if (dropList == null) { return; } - final List dropList = new ArrayList<>(templateList); Collections.sort(dropList, (d1, d2) -> Integer.valueOf(d1.getItemId()).compareTo(Integer.valueOf(d2.getItemId()))); int pages = dropList.size() / DROP_LIST_ITEMS_PER_PAGE; diff --git a/L2J_Mobius_08.2_Homunculus/dist/game/data/scripts/handlers/communityboard/DropSearchBoard.java b/L2J_Mobius_08.2_Homunculus/dist/game/data/scripts/handlers/communityboard/DropSearchBoard.java index 8ab335bc8b..94af43751f 100644 --- a/L2J_Mobius_08.2_Homunculus/dist/game/data/scripts/handlers/communityboard/DropSearchBoard.java +++ b/L2J_Mobius_08.2_Homunculus/dist/game/data/scripts/handlers/communityboard/DropSearchBoard.java @@ -36,6 +36,7 @@ import org.l2jmobius.gameserver.handler.CommunityBoardHandler; import org.l2jmobius.gameserver.handler.IParseBoardHandler; import org.l2jmobius.gameserver.model.actor.Player; import org.l2jmobius.gameserver.model.actor.templates.NpcTemplate; +import org.l2jmobius.gameserver.model.holders.DropGroupHolder; import org.l2jmobius.gameserver.model.holders.DropHolder; import org.l2jmobius.gameserver.model.item.ItemTemplate; import org.l2jmobius.gameserver.model.itemcontainer.Inventory; @@ -104,6 +105,17 @@ public class DropSearchBoard implements IParseBoardHandler private void buildDropIndex() { + NpcData.getInstance().getTemplates(npc -> npc.getDropGroups() != null).forEach(npcTemplate -> + { + for (DropGroupHolder dropGroup : npcTemplate.getDropGroups()) + { + final double chance = dropGroup.getChance() / 100; + for (DropHolder dropHolder : dropGroup.getDropList()) + { + addToDropList(npcTemplate, new DropHolder(dropHolder.getDropType(), dropHolder.getItemId(), dropHolder.getMin(), dropHolder.getMax(), dropHolder.getChance() * chance)); + } + } + }); NpcData.getInstance().getTemplates(npc -> npc.getDropList() != null).forEach(npcTemplate -> { for (DropHolder dropHolder : npcTemplate.getDropList()) diff --git a/L2J_Mobius_08.2_Homunculus/dist/game/data/xsd/npcs.xsd b/L2J_Mobius_08.2_Homunculus/dist/game/data/xsd/npcs.xsd index 70611293a3..9068efd10b 100644 --- a/L2J_Mobius_08.2_Homunculus/dist/game/data/xsd/npcs.xsd +++ b/L2J_Mobius_08.2_Homunculus/dist/game/data/xsd/npcs.xsd @@ -1,16 +1,5 @@ - - - - - - - - - - - @@ -260,11 +249,81 @@ - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/L2J_Mobius_08.2_Homunculus/java/org/l2jmobius/gameserver/data/xml/NpcData.java b/L2J_Mobius_08.2_Homunculus/java/org/l2jmobius/gameserver/data/xml/NpcData.java index 0d537566a1..5286a49d63 100644 --- a/L2J_Mobius_08.2_Homunculus/java/org/l2jmobius/gameserver/data/xml/NpcData.java +++ b/L2J_Mobius_08.2_Homunculus/java/org/l2jmobius/gameserver/data/xml/NpcData.java @@ -46,6 +46,7 @@ import org.l2jmobius.gameserver.enums.MpRewardType; import org.l2jmobius.gameserver.model.StatSet; import org.l2jmobius.gameserver.model.actor.templates.NpcTemplate; import org.l2jmobius.gameserver.model.effects.EffectType; +import org.l2jmobius.gameserver.model.holders.DropGroupHolder; import org.l2jmobius.gameserver.model.holders.DropHolder; import org.l2jmobius.gameserver.model.skill.Skill; @@ -100,6 +101,7 @@ public class NpcData implements IXmlReader Set clans = null; Set ignoreClanNpcIds = null; List dropLists = null; + List dropGroups = null; set.set("id", npcId); set.set("displayId", parseInteger(attrs, "displayId")); set.set("level", parseInteger(attrs, "level")); @@ -436,24 +438,54 @@ public class NpcData implements IXmlReader if (dropType != null) { - if (dropLists == null) - { - dropLists = new ArrayList<>(); - } - for (Node dropNode = dropListsNode.getFirstChild(); dropNode != null; dropNode = dropNode.getNextSibling()) { - final NamedNodeMap dropAttrs = dropNode.getAttributes(); - if ("item".equalsIgnoreCase(dropNode.getNodeName())) + final String nodeName = dropNode.getNodeName(); + if (nodeName.equalsIgnoreCase("group")) { - final DropHolder dropItem = new DropHolder(dropType, parseInteger(dropAttrs, "id"), parseLong(dropAttrs, "min"), parseLong(dropAttrs, "max"), parseDouble(dropAttrs, "chance")); - if (ItemTable.getInstance().getTemplate(parseInteger(dropAttrs, "id")) == null) + if (dropGroups == null) { - LOGGER.warning("DropListItem: Could not find item with id " + parseInteger(dropAttrs, "id") + "."); + dropGroups = new ArrayList<>(); + } + + final DropGroupHolder group = new DropGroupHolder(parseDouble(dropNode.getAttributes(), "chance")); + for (Node groupNode = dropNode.getFirstChild(); groupNode != null; groupNode = groupNode.getNextSibling()) + { + if (groupNode.getNodeName().equalsIgnoreCase("item")) + { + final NamedNodeMap groupAttrs = groupNode.getAttributes(); + final int itemId = parseInteger(groupAttrs, "id"); + + if (ItemTable.getInstance().getTemplate(itemId) == null) + { + LOGGER.warning("DropListItem: Could not find item with id " + itemId + "."); + } + else + { + group.addDrop(new DropHolder(dropType, itemId, parseLong(groupAttrs, "min"), parseLong(groupAttrs, "max"), parseDouble(groupAttrs, "chance"))); + } + } + } + + dropGroups.add(group); + } + else if (nodeName.equalsIgnoreCase("item")) + { + if (dropLists == null) + { + dropLists = new ArrayList<>(); + } + + final NamedNodeMap dropAttrs = dropNode.getAttributes(); + final int itemId = parseInteger(dropAttrs, "id"); + + if (ItemTable.getInstance().getTemplate(itemId) == null) + { + LOGGER.warning("DropListItem: Could not find item with id " + itemId + "."); } else { - dropLists.add(dropItem); + dropLists.add(new DropHolder(dropType, itemId, parseLong(dropAttrs, "min"), parseLong(dropAttrs, "max"), parseDouble(dropAttrs, "chance"))); } } } @@ -612,10 +644,21 @@ public class NpcData implements IXmlReader template.setClans(clans); template.setIgnoreClanNpcIds(ignoreClanNpcIds); + // Clean old drop groups. + template.removeDropGroups(); + + // Set new drop groups. + if (dropGroups != null) + { + template.setDropGroups(dropGroups); + } + + // Clean old drop lists. + template.removeDrops(); + + // Set new drop lists. if (dropLists != null) { - template.removeDrops(); - // Drops are sorted by chance (high to low). Collections.sort(dropLists, (d1, d2) -> Double.valueOf(d2.getChance()).compareTo(Double.valueOf(d1.getChance()))); for (DropHolder dropHolder : dropLists) diff --git a/L2J_Mobius_08.2_Homunculus/java/org/l2jmobius/gameserver/model/actor/templates/NpcTemplate.java b/L2J_Mobius_08.2_Homunculus/java/org/l2jmobius/gameserver/model/actor/templates/NpcTemplate.java index 05af374536..5237927054 100644 --- a/L2J_Mobius_08.2_Homunculus/java/org/l2jmobius/gameserver/model/actor/templates/NpcTemplate.java +++ b/L2J_Mobius_08.2_Homunculus/java/org/l2jmobius/gameserver/model/actor/templates/NpcTemplate.java @@ -36,6 +36,7 @@ import org.l2jmobius.gameserver.enums.Race; import org.l2jmobius.gameserver.enums.Sex; import org.l2jmobius.gameserver.model.StatSet; import org.l2jmobius.gameserver.model.actor.Creature; +import org.l2jmobius.gameserver.model.holders.DropGroupHolder; import org.l2jmobius.gameserver.model.holders.DropHolder; import org.l2jmobius.gameserver.model.holders.ItemHolder; import org.l2jmobius.gameserver.model.interfaces.IIdentifiable; @@ -107,6 +108,7 @@ public class NpcTemplate extends CreatureTemplate implements IIdentifiable private Map> _aiSkillLists; private Set _clans; private Set _ignoreClanNpcIds; + private List _dropGroups; private List _dropListDeath; private List _dropListSpoil; private float _collisionRadiusGrown; @@ -649,12 +651,22 @@ public class NpcTemplate extends CreatureTemplate implements IIdentifiable _ignoreClanNpcIds = ignoreClanNpcIds != null ? Collections.unmodifiableSet(ignoreClanNpcIds) : null; } + public void removeDropGroups() + { + _dropGroups = null; + } + public void removeDrops() { _dropListDeath = null; _dropListSpoil = null; } + public void setDropGroups(List groups) + { + _dropGroups = groups; + } + public void addDrop(DropHolder dropHolder) { if (_dropListDeath == null) @@ -673,6 +685,11 @@ public class NpcTemplate extends CreatureTemplate implements IIdentifiable _dropListSpoil.add(dropHolder); } + public List getDropGroups() + { + return _dropGroups; + } + public List getDropList() { return _dropListDeath; @@ -685,11 +702,201 @@ public class NpcTemplate extends CreatureTemplate implements IIdentifiable public List calculateDrops(DropType dropType, Creature victim, Creature killer) { - final List dropList = dropType == DropType.SPOIL ? _dropListSpoil : _dropListDeath; - if (dropList == null) + if (dropType == DropType.DROP) { - return null; + // calculate group drops + List groupDrops = null; + if (_dropGroups != null) + { + groupDrops = calculateGroupDrops(victim, killer); + } + + // calculate ungrouped drops + List ungroupedDrops = null; + if (_dropListDeath != null) + { + ungroupedDrops = calculateUngroupedDrops(dropType, victim, killer); + } + + // return results + if ((groupDrops != null) && (ungroupedDrops != null)) + { + groupDrops.addAll(ungroupedDrops); + ungroupedDrops.clear(); + return groupDrops; + } + if (groupDrops != null) + { + return groupDrops; + } + if (ungroupedDrops != null) + { + return ungroupedDrops; + } } + else if ((dropType == DropType.SPOIL) && (_dropListSpoil != null)) + { + return calculateUngroupedDrops(dropType, victim, killer); + } + + // no drops + return null; + } + + private List calculateGroupDrops(Creature victim, Creature killer) + { + // level difference calculations + final int levelDifference = victim.getLevel() - killer.getLevel(); + final double levelGapChanceToDropAdena = Util.map(levelDifference, -Config.DROP_ADENA_MAX_LEVEL_DIFFERENCE, -Config.DROP_ADENA_MIN_LEVEL_DIFFERENCE, Config.DROP_ADENA_MIN_LEVEL_GAP_CHANCE, 100d); + final double levelGapChanceToDrop = Util.map(levelDifference, -Config.DROP_ITEM_MAX_LEVEL_DIFFERENCE, -Config.DROP_ITEM_MIN_LEVEL_DIFFERENCE, Config.DROP_ITEM_MIN_LEVEL_GAP_CHANCE, 100d); + + int dropOccurrenceCounter = victim.isRaid() ? Config.DROP_MAX_OCCURRENCES_RAIDBOSS : Config.DROP_MAX_OCCURRENCES_NORMAL; + List calculatedDrops = null; + for (DropGroupHolder group : _dropGroups) + { + if (dropOccurrenceCounter <= 0) + { + break; + } + + double groupRate = 1; + final List groupDrops = group.getDropList(); + for (DropHolder dropItem : groupDrops) + { + final int itemId = dropItem.getItemId(); + final ItemTemplate item = ItemTable.getInstance().getTemplate(itemId); + final boolean champion = victim.isChampion(); + + // chance + double rateChance = 1; + final Float itemChance = Config.RATE_DROP_CHANCE_BY_ID.get(itemId); + if (itemChance != null) + { + if (itemChance == 0) + { + continue; + } + + rateChance *= itemChance; + if (champion && (itemId == Inventory.ADENA_ID)) + { + rateChance *= Config.CHAMPION_ADENAS_REWARDS_CHANCE; + } + } + else if (item.hasExImmediateEffect()) + { + rateChance *= Config.RATE_HERB_DROP_CHANCE_MULTIPLIER; + } + else if (victim.isRaid()) + { + rateChance *= Config.RATE_RAID_DROP_CHANCE_MULTIPLIER; + } + else + { + rateChance *= Config.RATE_DEATH_DROP_CHANCE_MULTIPLIER * (champion ? Config.CHAMPION_REWARDS_CHANCE : 1); + } + + // premium chance + if (Config.PREMIUM_SYSTEM_ENABLED && (killer.getActingPlayer() != null) && killer.getActingPlayer().hasPremiumStatus()) + { + if (Config.PREMIUM_RATE_DROP_CHANCE_BY_ID.get(itemId) != null) + { + rateChance *= Config.PREMIUM_RATE_DROP_CHANCE_BY_ID.get(itemId); + } + else if (item.hasExImmediateEffect()) + { + // TODO: Premium herb chance? :) + } + else if (victim.isRaid()) + { + // TODO: Premium raid chance? :) + } + else + { + rateChance *= Config.PREMIUM_RATE_DROP_CHANCE; + } + } + + // keep lowest to avoid chance by id configuration conflicts + groupRate = Math.min(groupRate, rateChance); + } + + if ((Rnd.nextDouble() * 100) < (group.getChance() * groupRate)) + { + double totalChance = 0; // total group chance is 100 + final double dropChance = Rnd.nextDouble() * 100; + GROUP_DROP: for (DropHolder dropItem : groupDrops) + { + if (dropOccurrenceCounter <= 0) + { + break; + } + + // calculate if item will drop + totalChance += dropItem.getChance(); + if (dropChance >= totalChance) + { + continue; + } + + // check level gap that may prevent to drop item + if ((Rnd.nextDouble() * 100) > (dropItem.getItemId() == Inventory.ADENA_ID ? levelGapChanceToDropAdena : levelGapChanceToDrop)) + { + continue; + } + + // create the drop + final ItemHolder drop = calculateGroupDrop(dropItem, victim, killer); + + // create list + if (calculatedDrops == null) + { + calculatedDrops = new ArrayList<>(dropOccurrenceCounter); + } + + // finally + if (group.getChance() < 100) + { + dropOccurrenceCounter--; + } + calculatedDrops.add(drop); + + // no more drops from this group + break GROUP_DROP; + } + } + } + + // champion extra drop + if (victim.isChampion()) + { + if ((victim.getLevel() < killer.getLevel()) && (Rnd.get(100) < Config.CHAMPION_REWARD_LOWER_LEVEL_ITEM_CHANCE)) + { + return calculatedDrops; + } + if ((victim.getLevel() > killer.getLevel()) && (Rnd.get(100) < Config.CHAMPION_REWARD_HIGHER_LEVEL_ITEM_CHANCE)) + { + return calculatedDrops; + } + + // create list + if (calculatedDrops == null) + { + calculatedDrops = new ArrayList<>(); + } + + if (!calculatedDrops.containsAll(Config.CHAMPION_REWARD_ITEMS)) + { + calculatedDrops.addAll(Config.CHAMPION_REWARD_ITEMS); + } + } + + return calculatedDrops; + } + + private List calculateUngroupedDrops(DropType dropType, Creature victim, Creature killer) + { + final List dropList = dropType == DropType.SPOIL ? _dropListSpoil : _dropListDeath; // level difference calculations final int levelDifference = victim.getLevel() - killer.getLevel(); @@ -721,7 +928,7 @@ public class NpcTemplate extends CreatureTemplate implements IIdentifiable } // calculate chances - final ItemHolder drop = calculateDrop(dropItem, victim, killer); + final ItemHolder drop = calculateUngroupedDrop(dropItem, victim, killer); if (drop == null) { continue; @@ -776,20 +983,82 @@ public class NpcTemplate extends CreatureTemplate implements IIdentifiable calculatedDrops = new ArrayList<>(); } - calculatedDrops.addAll(Config.CHAMPION_REWARD_ITEMS); + if (!calculatedDrops.containsAll(Config.CHAMPION_REWARD_ITEMS)) + { + calculatedDrops.addAll(Config.CHAMPION_REWARD_ITEMS); + } } return calculatedDrops; } /** - * All item drop chance calculations are done by this method. * @param dropItem * @param victim * @param killer * @return ItemHolder */ - private ItemHolder calculateDrop(DropHolder dropItem, Creature victim, Creature killer) + private ItemHolder calculateGroupDrop(DropHolder dropItem, Creature victim, Creature killer) + { + final int itemId = dropItem.getItemId(); + final ItemTemplate item = ItemTable.getInstance().getTemplate(itemId); + final boolean champion = victim.isChampion(); + + // calculate amount + double rateAmount = 1; + if (Config.RATE_DROP_AMOUNT_BY_ID.get(itemId) != null) + { + rateAmount *= Config.RATE_DROP_AMOUNT_BY_ID.get(itemId); + if (champion && (itemId == Inventory.ADENA_ID)) + { + rateAmount *= Config.CHAMPION_ADENAS_REWARDS_AMOUNT; + } + } + else if (item.hasExImmediateEffect()) + { + rateAmount *= Config.RATE_HERB_DROP_AMOUNT_MULTIPLIER; + } + else if (victim.isRaid()) + { + rateAmount *= Config.RATE_RAID_DROP_AMOUNT_MULTIPLIER; + } + else + { + rateAmount *= Config.RATE_DEATH_DROP_AMOUNT_MULTIPLIER * (champion ? Config.CHAMPION_REWARDS_AMOUNT : 1); + } + + // premium amount + if (Config.PREMIUM_SYSTEM_ENABLED && (killer.getActingPlayer() != null) && killer.getActingPlayer().hasPremiumStatus()) + { + if (Config.PREMIUM_RATE_DROP_AMOUNT_BY_ID.get(itemId) != null) + { + rateAmount *= Config.PREMIUM_RATE_DROP_AMOUNT_BY_ID.get(itemId); + } + else if (item.hasExImmediateEffect()) + { + // TODO: Premium herb amount? :) + } + else if (victim.isRaid()) + { + // TODO: Premium raid amount? :) + } + else + { + rateAmount *= Config.PREMIUM_RATE_DROP_AMOUNT; + } + } + + // finally + return new ItemHolder(itemId, (long) (Rnd.get(dropItem.getMin(), dropItem.getMax()) * rateAmount)); + } + + /** + * @param dropItem + * @param victim + * @param killer + * @return ItemHolder + */ + private ItemHolder calculateUngroupedDrop(DropHolder dropItem, Creature victim, Creature killer) { switch (dropItem.getDropType()) { diff --git a/L2J_Mobius_08.2_Homunculus/java/org/l2jmobius/gameserver/model/holders/DropGroupHolder.java b/L2J_Mobius_08.2_Homunculus/java/org/l2jmobius/gameserver/model/holders/DropGroupHolder.java new file mode 100644 index 0000000000..6c2401c0b0 --- /dev/null +++ b/L2J_Mobius_08.2_Homunculus/java/org/l2jmobius/gameserver/model/holders/DropGroupHolder.java @@ -0,0 +1,49 @@ +/* + * This file is part of the L2J Mobius project. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package org.l2jmobius.gameserver.model.holders; + +import java.util.ArrayList; +import java.util.List; + +/** + * @author Mobius + */ +public class DropGroupHolder +{ + private final List _dropList = new ArrayList<>(); + private final double _chance; + + public DropGroupHolder(double chance) + { + _chance = chance; + } + + public List getDropList() + { + return _dropList; + } + + public void addDrop(DropHolder holder) + { + _dropList.add(holder); + } + + public double getChance() + { + return _chance; + } +} diff --git a/L2J_Mobius_09.2_ReturnOfTheQueenAnt/dist/game/data/scripts/handlers/bypasshandlers/NpcViewMod.java b/L2J_Mobius_09.2_ReturnOfTheQueenAnt/dist/game/data/scripts/handlers/bypasshandlers/NpcViewMod.java index 35d31315ad..fab79abca8 100644 --- a/L2J_Mobius_09.2_ReturnOfTheQueenAnt/dist/game/data/scripts/handlers/bypasshandlers/NpcViewMod.java +++ b/L2J_Mobius_09.2_ReturnOfTheQueenAnt/dist/game/data/scripts/handlers/bypasshandlers/NpcViewMod.java @@ -37,6 +37,7 @@ import org.l2jmobius.gameserver.model.actor.Attackable; import org.l2jmobius.gameserver.model.actor.Creature; import org.l2jmobius.gameserver.model.actor.Npc; import org.l2jmobius.gameserver.model.actor.Player; +import org.l2jmobius.gameserver.model.holders.DropGroupHolder; import org.l2jmobius.gameserver.model.holders.DropHolder; import org.l2jmobius.gameserver.model.item.ItemTemplate; import org.l2jmobius.gameserver.model.itemcontainer.Inventory; @@ -326,12 +327,13 @@ public class NpcViewMod implements IBypassHandler private static String getDropListButtons(Npc npc) { final StringBuilder sb = new StringBuilder(); + final List dropListGroups = npc.getTemplate().getDropGroups(); final List dropListDeath = npc.getTemplate().getDropList(); final List dropListSpoil = npc.getTemplate().getSpoilList(); - if ((dropListDeath != null) || (dropListSpoil != null)) + if ((dropListGroups != null) || (dropListDeath != null) || (dropListSpoil != null)) { sb.append("
"); - if (dropListDeath != null) + if ((dropListGroups != null) || (dropListDeath != null)) { sb.append(""); } @@ -348,13 +350,40 @@ public class NpcViewMod implements IBypassHandler private void sendNpcDropList(Player player, Npc npc, DropType dropType, int pageValue) { - final List templateList = dropType == DropType.SPOIL ? npc.getTemplate().getSpoilList() : npc.getTemplate().getDropList(); - if (templateList == null) + List dropList = null; + if (dropType == DropType.SPOIL) + { + dropList = new ArrayList<>(npc.getTemplate().getSpoilList()); + } + else + { + final List drops = npc.getTemplate().getDropList(); + if (drops != null) + { + dropList = new ArrayList<>(drops); + } + final List dropGroups = npc.getTemplate().getDropGroups(); + if (dropGroups != null) + { + if (dropList == null) + { + dropList = new ArrayList<>(); + } + for (DropGroupHolder dropGroup : dropGroups) + { + final double chance = dropGroup.getChance() / 100; + for (DropHolder dropHolder : dropGroup.getDropList()) + { + dropList.add(new DropHolder(dropHolder.getDropType(), dropHolder.getItemId(), dropHolder.getMin(), dropHolder.getMax(), dropHolder.getChance() * chance)); + } + } + } + } + if (dropList == null) { return; } - final List dropList = new ArrayList<>(templateList); Collections.sort(dropList, (d1, d2) -> Integer.valueOf(d1.getItemId()).compareTo(Integer.valueOf(d2.getItemId()))); int pages = dropList.size() / DROP_LIST_ITEMS_PER_PAGE; diff --git a/L2J_Mobius_09.2_ReturnOfTheQueenAnt/dist/game/data/scripts/handlers/communityboard/DropSearchBoard.java b/L2J_Mobius_09.2_ReturnOfTheQueenAnt/dist/game/data/scripts/handlers/communityboard/DropSearchBoard.java index 8ab335bc8b..94af43751f 100644 --- a/L2J_Mobius_09.2_ReturnOfTheQueenAnt/dist/game/data/scripts/handlers/communityboard/DropSearchBoard.java +++ b/L2J_Mobius_09.2_ReturnOfTheQueenAnt/dist/game/data/scripts/handlers/communityboard/DropSearchBoard.java @@ -36,6 +36,7 @@ import org.l2jmobius.gameserver.handler.CommunityBoardHandler; import org.l2jmobius.gameserver.handler.IParseBoardHandler; import org.l2jmobius.gameserver.model.actor.Player; import org.l2jmobius.gameserver.model.actor.templates.NpcTemplate; +import org.l2jmobius.gameserver.model.holders.DropGroupHolder; import org.l2jmobius.gameserver.model.holders.DropHolder; import org.l2jmobius.gameserver.model.item.ItemTemplate; import org.l2jmobius.gameserver.model.itemcontainer.Inventory; @@ -104,6 +105,17 @@ public class DropSearchBoard implements IParseBoardHandler private void buildDropIndex() { + NpcData.getInstance().getTemplates(npc -> npc.getDropGroups() != null).forEach(npcTemplate -> + { + for (DropGroupHolder dropGroup : npcTemplate.getDropGroups()) + { + final double chance = dropGroup.getChance() / 100; + for (DropHolder dropHolder : dropGroup.getDropList()) + { + addToDropList(npcTemplate, new DropHolder(dropHolder.getDropType(), dropHolder.getItemId(), dropHolder.getMin(), dropHolder.getMax(), dropHolder.getChance() * chance)); + } + } + }); NpcData.getInstance().getTemplates(npc -> npc.getDropList() != null).forEach(npcTemplate -> { for (DropHolder dropHolder : npcTemplate.getDropList()) diff --git a/L2J_Mobius_09.2_ReturnOfTheQueenAnt/dist/game/data/xsd/npcs.xsd b/L2J_Mobius_09.2_ReturnOfTheQueenAnt/dist/game/data/xsd/npcs.xsd index 70611293a3..9068efd10b 100644 --- a/L2J_Mobius_09.2_ReturnOfTheQueenAnt/dist/game/data/xsd/npcs.xsd +++ b/L2J_Mobius_09.2_ReturnOfTheQueenAnt/dist/game/data/xsd/npcs.xsd @@ -1,16 +1,5 @@ - - - - - - - - - - - @@ -260,11 +249,81 @@ - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/L2J_Mobius_09.2_ReturnOfTheQueenAnt/java/org/l2jmobius/gameserver/data/xml/NpcData.java b/L2J_Mobius_09.2_ReturnOfTheQueenAnt/java/org/l2jmobius/gameserver/data/xml/NpcData.java index 0d537566a1..5286a49d63 100644 --- a/L2J_Mobius_09.2_ReturnOfTheQueenAnt/java/org/l2jmobius/gameserver/data/xml/NpcData.java +++ b/L2J_Mobius_09.2_ReturnOfTheQueenAnt/java/org/l2jmobius/gameserver/data/xml/NpcData.java @@ -46,6 +46,7 @@ import org.l2jmobius.gameserver.enums.MpRewardType; import org.l2jmobius.gameserver.model.StatSet; import org.l2jmobius.gameserver.model.actor.templates.NpcTemplate; import org.l2jmobius.gameserver.model.effects.EffectType; +import org.l2jmobius.gameserver.model.holders.DropGroupHolder; import org.l2jmobius.gameserver.model.holders.DropHolder; import org.l2jmobius.gameserver.model.skill.Skill; @@ -100,6 +101,7 @@ public class NpcData implements IXmlReader Set clans = null; Set ignoreClanNpcIds = null; List dropLists = null; + List dropGroups = null; set.set("id", npcId); set.set("displayId", parseInteger(attrs, "displayId")); set.set("level", parseInteger(attrs, "level")); @@ -436,24 +438,54 @@ public class NpcData implements IXmlReader if (dropType != null) { - if (dropLists == null) - { - dropLists = new ArrayList<>(); - } - for (Node dropNode = dropListsNode.getFirstChild(); dropNode != null; dropNode = dropNode.getNextSibling()) { - final NamedNodeMap dropAttrs = dropNode.getAttributes(); - if ("item".equalsIgnoreCase(dropNode.getNodeName())) + final String nodeName = dropNode.getNodeName(); + if (nodeName.equalsIgnoreCase("group")) { - final DropHolder dropItem = new DropHolder(dropType, parseInteger(dropAttrs, "id"), parseLong(dropAttrs, "min"), parseLong(dropAttrs, "max"), parseDouble(dropAttrs, "chance")); - if (ItemTable.getInstance().getTemplate(parseInteger(dropAttrs, "id")) == null) + if (dropGroups == null) { - LOGGER.warning("DropListItem: Could not find item with id " + parseInteger(dropAttrs, "id") + "."); + dropGroups = new ArrayList<>(); + } + + final DropGroupHolder group = new DropGroupHolder(parseDouble(dropNode.getAttributes(), "chance")); + for (Node groupNode = dropNode.getFirstChild(); groupNode != null; groupNode = groupNode.getNextSibling()) + { + if (groupNode.getNodeName().equalsIgnoreCase("item")) + { + final NamedNodeMap groupAttrs = groupNode.getAttributes(); + final int itemId = parseInteger(groupAttrs, "id"); + + if (ItemTable.getInstance().getTemplate(itemId) == null) + { + LOGGER.warning("DropListItem: Could not find item with id " + itemId + "."); + } + else + { + group.addDrop(new DropHolder(dropType, itemId, parseLong(groupAttrs, "min"), parseLong(groupAttrs, "max"), parseDouble(groupAttrs, "chance"))); + } + } + } + + dropGroups.add(group); + } + else if (nodeName.equalsIgnoreCase("item")) + { + if (dropLists == null) + { + dropLists = new ArrayList<>(); + } + + final NamedNodeMap dropAttrs = dropNode.getAttributes(); + final int itemId = parseInteger(dropAttrs, "id"); + + if (ItemTable.getInstance().getTemplate(itemId) == null) + { + LOGGER.warning("DropListItem: Could not find item with id " + itemId + "."); } else { - dropLists.add(dropItem); + dropLists.add(new DropHolder(dropType, itemId, parseLong(dropAttrs, "min"), parseLong(dropAttrs, "max"), parseDouble(dropAttrs, "chance"))); } } } @@ -612,10 +644,21 @@ public class NpcData implements IXmlReader template.setClans(clans); template.setIgnoreClanNpcIds(ignoreClanNpcIds); + // Clean old drop groups. + template.removeDropGroups(); + + // Set new drop groups. + if (dropGroups != null) + { + template.setDropGroups(dropGroups); + } + + // Clean old drop lists. + template.removeDrops(); + + // Set new drop lists. if (dropLists != null) { - template.removeDrops(); - // Drops are sorted by chance (high to low). Collections.sort(dropLists, (d1, d2) -> Double.valueOf(d2.getChance()).compareTo(Double.valueOf(d1.getChance()))); for (DropHolder dropHolder : dropLists) diff --git a/L2J_Mobius_09.2_ReturnOfTheQueenAnt/java/org/l2jmobius/gameserver/model/actor/templates/NpcTemplate.java b/L2J_Mobius_09.2_ReturnOfTheQueenAnt/java/org/l2jmobius/gameserver/model/actor/templates/NpcTemplate.java index 05af374536..5237927054 100644 --- a/L2J_Mobius_09.2_ReturnOfTheQueenAnt/java/org/l2jmobius/gameserver/model/actor/templates/NpcTemplate.java +++ b/L2J_Mobius_09.2_ReturnOfTheQueenAnt/java/org/l2jmobius/gameserver/model/actor/templates/NpcTemplate.java @@ -36,6 +36,7 @@ import org.l2jmobius.gameserver.enums.Race; import org.l2jmobius.gameserver.enums.Sex; import org.l2jmobius.gameserver.model.StatSet; import org.l2jmobius.gameserver.model.actor.Creature; +import org.l2jmobius.gameserver.model.holders.DropGroupHolder; import org.l2jmobius.gameserver.model.holders.DropHolder; import org.l2jmobius.gameserver.model.holders.ItemHolder; import org.l2jmobius.gameserver.model.interfaces.IIdentifiable; @@ -107,6 +108,7 @@ public class NpcTemplate extends CreatureTemplate implements IIdentifiable private Map> _aiSkillLists; private Set _clans; private Set _ignoreClanNpcIds; + private List _dropGroups; private List _dropListDeath; private List _dropListSpoil; private float _collisionRadiusGrown; @@ -649,12 +651,22 @@ public class NpcTemplate extends CreatureTemplate implements IIdentifiable _ignoreClanNpcIds = ignoreClanNpcIds != null ? Collections.unmodifiableSet(ignoreClanNpcIds) : null; } + public void removeDropGroups() + { + _dropGroups = null; + } + public void removeDrops() { _dropListDeath = null; _dropListSpoil = null; } + public void setDropGroups(List groups) + { + _dropGroups = groups; + } + public void addDrop(DropHolder dropHolder) { if (_dropListDeath == null) @@ -673,6 +685,11 @@ public class NpcTemplate extends CreatureTemplate implements IIdentifiable _dropListSpoil.add(dropHolder); } + public List getDropGroups() + { + return _dropGroups; + } + public List getDropList() { return _dropListDeath; @@ -685,11 +702,201 @@ public class NpcTemplate extends CreatureTemplate implements IIdentifiable public List calculateDrops(DropType dropType, Creature victim, Creature killer) { - final List dropList = dropType == DropType.SPOIL ? _dropListSpoil : _dropListDeath; - if (dropList == null) + if (dropType == DropType.DROP) { - return null; + // calculate group drops + List groupDrops = null; + if (_dropGroups != null) + { + groupDrops = calculateGroupDrops(victim, killer); + } + + // calculate ungrouped drops + List ungroupedDrops = null; + if (_dropListDeath != null) + { + ungroupedDrops = calculateUngroupedDrops(dropType, victim, killer); + } + + // return results + if ((groupDrops != null) && (ungroupedDrops != null)) + { + groupDrops.addAll(ungroupedDrops); + ungroupedDrops.clear(); + return groupDrops; + } + if (groupDrops != null) + { + return groupDrops; + } + if (ungroupedDrops != null) + { + return ungroupedDrops; + } } + else if ((dropType == DropType.SPOIL) && (_dropListSpoil != null)) + { + return calculateUngroupedDrops(dropType, victim, killer); + } + + // no drops + return null; + } + + private List calculateGroupDrops(Creature victim, Creature killer) + { + // level difference calculations + final int levelDifference = victim.getLevel() - killer.getLevel(); + final double levelGapChanceToDropAdena = Util.map(levelDifference, -Config.DROP_ADENA_MAX_LEVEL_DIFFERENCE, -Config.DROP_ADENA_MIN_LEVEL_DIFFERENCE, Config.DROP_ADENA_MIN_LEVEL_GAP_CHANCE, 100d); + final double levelGapChanceToDrop = Util.map(levelDifference, -Config.DROP_ITEM_MAX_LEVEL_DIFFERENCE, -Config.DROP_ITEM_MIN_LEVEL_DIFFERENCE, Config.DROP_ITEM_MIN_LEVEL_GAP_CHANCE, 100d); + + int dropOccurrenceCounter = victim.isRaid() ? Config.DROP_MAX_OCCURRENCES_RAIDBOSS : Config.DROP_MAX_OCCURRENCES_NORMAL; + List calculatedDrops = null; + for (DropGroupHolder group : _dropGroups) + { + if (dropOccurrenceCounter <= 0) + { + break; + } + + double groupRate = 1; + final List groupDrops = group.getDropList(); + for (DropHolder dropItem : groupDrops) + { + final int itemId = dropItem.getItemId(); + final ItemTemplate item = ItemTable.getInstance().getTemplate(itemId); + final boolean champion = victim.isChampion(); + + // chance + double rateChance = 1; + final Float itemChance = Config.RATE_DROP_CHANCE_BY_ID.get(itemId); + if (itemChance != null) + { + if (itemChance == 0) + { + continue; + } + + rateChance *= itemChance; + if (champion && (itemId == Inventory.ADENA_ID)) + { + rateChance *= Config.CHAMPION_ADENAS_REWARDS_CHANCE; + } + } + else if (item.hasExImmediateEffect()) + { + rateChance *= Config.RATE_HERB_DROP_CHANCE_MULTIPLIER; + } + else if (victim.isRaid()) + { + rateChance *= Config.RATE_RAID_DROP_CHANCE_MULTIPLIER; + } + else + { + rateChance *= Config.RATE_DEATH_DROP_CHANCE_MULTIPLIER * (champion ? Config.CHAMPION_REWARDS_CHANCE : 1); + } + + // premium chance + if (Config.PREMIUM_SYSTEM_ENABLED && (killer.getActingPlayer() != null) && killer.getActingPlayer().hasPremiumStatus()) + { + if (Config.PREMIUM_RATE_DROP_CHANCE_BY_ID.get(itemId) != null) + { + rateChance *= Config.PREMIUM_RATE_DROP_CHANCE_BY_ID.get(itemId); + } + else if (item.hasExImmediateEffect()) + { + // TODO: Premium herb chance? :) + } + else if (victim.isRaid()) + { + // TODO: Premium raid chance? :) + } + else + { + rateChance *= Config.PREMIUM_RATE_DROP_CHANCE; + } + } + + // keep lowest to avoid chance by id configuration conflicts + groupRate = Math.min(groupRate, rateChance); + } + + if ((Rnd.nextDouble() * 100) < (group.getChance() * groupRate)) + { + double totalChance = 0; // total group chance is 100 + final double dropChance = Rnd.nextDouble() * 100; + GROUP_DROP: for (DropHolder dropItem : groupDrops) + { + if (dropOccurrenceCounter <= 0) + { + break; + } + + // calculate if item will drop + totalChance += dropItem.getChance(); + if (dropChance >= totalChance) + { + continue; + } + + // check level gap that may prevent to drop item + if ((Rnd.nextDouble() * 100) > (dropItem.getItemId() == Inventory.ADENA_ID ? levelGapChanceToDropAdena : levelGapChanceToDrop)) + { + continue; + } + + // create the drop + final ItemHolder drop = calculateGroupDrop(dropItem, victim, killer); + + // create list + if (calculatedDrops == null) + { + calculatedDrops = new ArrayList<>(dropOccurrenceCounter); + } + + // finally + if (group.getChance() < 100) + { + dropOccurrenceCounter--; + } + calculatedDrops.add(drop); + + // no more drops from this group + break GROUP_DROP; + } + } + } + + // champion extra drop + if (victim.isChampion()) + { + if ((victim.getLevel() < killer.getLevel()) && (Rnd.get(100) < Config.CHAMPION_REWARD_LOWER_LEVEL_ITEM_CHANCE)) + { + return calculatedDrops; + } + if ((victim.getLevel() > killer.getLevel()) && (Rnd.get(100) < Config.CHAMPION_REWARD_HIGHER_LEVEL_ITEM_CHANCE)) + { + return calculatedDrops; + } + + // create list + if (calculatedDrops == null) + { + calculatedDrops = new ArrayList<>(); + } + + if (!calculatedDrops.containsAll(Config.CHAMPION_REWARD_ITEMS)) + { + calculatedDrops.addAll(Config.CHAMPION_REWARD_ITEMS); + } + } + + return calculatedDrops; + } + + private List calculateUngroupedDrops(DropType dropType, Creature victim, Creature killer) + { + final List dropList = dropType == DropType.SPOIL ? _dropListSpoil : _dropListDeath; // level difference calculations final int levelDifference = victim.getLevel() - killer.getLevel(); @@ -721,7 +928,7 @@ public class NpcTemplate extends CreatureTemplate implements IIdentifiable } // calculate chances - final ItemHolder drop = calculateDrop(dropItem, victim, killer); + final ItemHolder drop = calculateUngroupedDrop(dropItem, victim, killer); if (drop == null) { continue; @@ -776,20 +983,82 @@ public class NpcTemplate extends CreatureTemplate implements IIdentifiable calculatedDrops = new ArrayList<>(); } - calculatedDrops.addAll(Config.CHAMPION_REWARD_ITEMS); + if (!calculatedDrops.containsAll(Config.CHAMPION_REWARD_ITEMS)) + { + calculatedDrops.addAll(Config.CHAMPION_REWARD_ITEMS); + } } return calculatedDrops; } /** - * All item drop chance calculations are done by this method. * @param dropItem * @param victim * @param killer * @return ItemHolder */ - private ItemHolder calculateDrop(DropHolder dropItem, Creature victim, Creature killer) + private ItemHolder calculateGroupDrop(DropHolder dropItem, Creature victim, Creature killer) + { + final int itemId = dropItem.getItemId(); + final ItemTemplate item = ItemTable.getInstance().getTemplate(itemId); + final boolean champion = victim.isChampion(); + + // calculate amount + double rateAmount = 1; + if (Config.RATE_DROP_AMOUNT_BY_ID.get(itemId) != null) + { + rateAmount *= Config.RATE_DROP_AMOUNT_BY_ID.get(itemId); + if (champion && (itemId == Inventory.ADENA_ID)) + { + rateAmount *= Config.CHAMPION_ADENAS_REWARDS_AMOUNT; + } + } + else if (item.hasExImmediateEffect()) + { + rateAmount *= Config.RATE_HERB_DROP_AMOUNT_MULTIPLIER; + } + else if (victim.isRaid()) + { + rateAmount *= Config.RATE_RAID_DROP_AMOUNT_MULTIPLIER; + } + else + { + rateAmount *= Config.RATE_DEATH_DROP_AMOUNT_MULTIPLIER * (champion ? Config.CHAMPION_REWARDS_AMOUNT : 1); + } + + // premium amount + if (Config.PREMIUM_SYSTEM_ENABLED && (killer.getActingPlayer() != null) && killer.getActingPlayer().hasPremiumStatus()) + { + if (Config.PREMIUM_RATE_DROP_AMOUNT_BY_ID.get(itemId) != null) + { + rateAmount *= Config.PREMIUM_RATE_DROP_AMOUNT_BY_ID.get(itemId); + } + else if (item.hasExImmediateEffect()) + { + // TODO: Premium herb amount? :) + } + else if (victim.isRaid()) + { + // TODO: Premium raid amount? :) + } + else + { + rateAmount *= Config.PREMIUM_RATE_DROP_AMOUNT; + } + } + + // finally + return new ItemHolder(itemId, (long) (Rnd.get(dropItem.getMin(), dropItem.getMax()) * rateAmount)); + } + + /** + * @param dropItem + * @param victim + * @param killer + * @return ItemHolder + */ + private ItemHolder calculateUngroupedDrop(DropHolder dropItem, Creature victim, Creature killer) { switch (dropItem.getDropType()) { diff --git a/L2J_Mobius_09.2_ReturnOfTheQueenAnt/java/org/l2jmobius/gameserver/model/holders/DropGroupHolder.java b/L2J_Mobius_09.2_ReturnOfTheQueenAnt/java/org/l2jmobius/gameserver/model/holders/DropGroupHolder.java new file mode 100644 index 0000000000..6c2401c0b0 --- /dev/null +++ b/L2J_Mobius_09.2_ReturnOfTheQueenAnt/java/org/l2jmobius/gameserver/model/holders/DropGroupHolder.java @@ -0,0 +1,49 @@ +/* + * This file is part of the L2J Mobius project. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package org.l2jmobius.gameserver.model.holders; + +import java.util.ArrayList; +import java.util.List; + +/** + * @author Mobius + */ +public class DropGroupHolder +{ + private final List _dropList = new ArrayList<>(); + private final double _chance; + + public DropGroupHolder(double chance) + { + _chance = chance; + } + + public List getDropList() + { + return _dropList; + } + + public void addDrop(DropHolder holder) + { + _dropList.add(holder); + } + + public double getChance() + { + return _chance; + } +} diff --git a/L2J_Mobius_10.1_MasterClass/dist/game/data/scripts/handlers/bypasshandlers/NpcViewMod.java b/L2J_Mobius_10.1_MasterClass/dist/game/data/scripts/handlers/bypasshandlers/NpcViewMod.java index 35d31315ad..fab79abca8 100644 --- a/L2J_Mobius_10.1_MasterClass/dist/game/data/scripts/handlers/bypasshandlers/NpcViewMod.java +++ b/L2J_Mobius_10.1_MasterClass/dist/game/data/scripts/handlers/bypasshandlers/NpcViewMod.java @@ -37,6 +37,7 @@ import org.l2jmobius.gameserver.model.actor.Attackable; import org.l2jmobius.gameserver.model.actor.Creature; import org.l2jmobius.gameserver.model.actor.Npc; import org.l2jmobius.gameserver.model.actor.Player; +import org.l2jmobius.gameserver.model.holders.DropGroupHolder; import org.l2jmobius.gameserver.model.holders.DropHolder; import org.l2jmobius.gameserver.model.item.ItemTemplate; import org.l2jmobius.gameserver.model.itemcontainer.Inventory; @@ -326,12 +327,13 @@ public class NpcViewMod implements IBypassHandler private static String getDropListButtons(Npc npc) { final StringBuilder sb = new StringBuilder(); + final List dropListGroups = npc.getTemplate().getDropGroups(); final List dropListDeath = npc.getTemplate().getDropList(); final List dropListSpoil = npc.getTemplate().getSpoilList(); - if ((dropListDeath != null) || (dropListSpoil != null)) + if ((dropListGroups != null) || (dropListDeath != null) || (dropListSpoil != null)) { sb.append("
"); - if (dropListDeath != null) + if ((dropListGroups != null) || (dropListDeath != null)) { sb.append(""); } @@ -348,13 +350,40 @@ public class NpcViewMod implements IBypassHandler private void sendNpcDropList(Player player, Npc npc, DropType dropType, int pageValue) { - final List templateList = dropType == DropType.SPOIL ? npc.getTemplate().getSpoilList() : npc.getTemplate().getDropList(); - if (templateList == null) + List dropList = null; + if (dropType == DropType.SPOIL) + { + dropList = new ArrayList<>(npc.getTemplate().getSpoilList()); + } + else + { + final List drops = npc.getTemplate().getDropList(); + if (drops != null) + { + dropList = new ArrayList<>(drops); + } + final List dropGroups = npc.getTemplate().getDropGroups(); + if (dropGroups != null) + { + if (dropList == null) + { + dropList = new ArrayList<>(); + } + for (DropGroupHolder dropGroup : dropGroups) + { + final double chance = dropGroup.getChance() / 100; + for (DropHolder dropHolder : dropGroup.getDropList()) + { + dropList.add(new DropHolder(dropHolder.getDropType(), dropHolder.getItemId(), dropHolder.getMin(), dropHolder.getMax(), dropHolder.getChance() * chance)); + } + } + } + } + if (dropList == null) { return; } - final List dropList = new ArrayList<>(templateList); Collections.sort(dropList, (d1, d2) -> Integer.valueOf(d1.getItemId()).compareTo(Integer.valueOf(d2.getItemId()))); int pages = dropList.size() / DROP_LIST_ITEMS_PER_PAGE; diff --git a/L2J_Mobius_10.1_MasterClass/dist/game/data/scripts/handlers/communityboard/DropSearchBoard.java b/L2J_Mobius_10.1_MasterClass/dist/game/data/scripts/handlers/communityboard/DropSearchBoard.java index 8ab335bc8b..94af43751f 100644 --- a/L2J_Mobius_10.1_MasterClass/dist/game/data/scripts/handlers/communityboard/DropSearchBoard.java +++ b/L2J_Mobius_10.1_MasterClass/dist/game/data/scripts/handlers/communityboard/DropSearchBoard.java @@ -36,6 +36,7 @@ import org.l2jmobius.gameserver.handler.CommunityBoardHandler; import org.l2jmobius.gameserver.handler.IParseBoardHandler; import org.l2jmobius.gameserver.model.actor.Player; import org.l2jmobius.gameserver.model.actor.templates.NpcTemplate; +import org.l2jmobius.gameserver.model.holders.DropGroupHolder; import org.l2jmobius.gameserver.model.holders.DropHolder; import org.l2jmobius.gameserver.model.item.ItemTemplate; import org.l2jmobius.gameserver.model.itemcontainer.Inventory; @@ -104,6 +105,17 @@ public class DropSearchBoard implements IParseBoardHandler private void buildDropIndex() { + NpcData.getInstance().getTemplates(npc -> npc.getDropGroups() != null).forEach(npcTemplate -> + { + for (DropGroupHolder dropGroup : npcTemplate.getDropGroups()) + { + final double chance = dropGroup.getChance() / 100; + for (DropHolder dropHolder : dropGroup.getDropList()) + { + addToDropList(npcTemplate, new DropHolder(dropHolder.getDropType(), dropHolder.getItemId(), dropHolder.getMin(), dropHolder.getMax(), dropHolder.getChance() * chance)); + } + } + }); NpcData.getInstance().getTemplates(npc -> npc.getDropList() != null).forEach(npcTemplate -> { for (DropHolder dropHolder : npcTemplate.getDropList()) diff --git a/L2J_Mobius_10.1_MasterClass/dist/game/data/xsd/npcs.xsd b/L2J_Mobius_10.1_MasterClass/dist/game/data/xsd/npcs.xsd index 70611293a3..9068efd10b 100644 --- a/L2J_Mobius_10.1_MasterClass/dist/game/data/xsd/npcs.xsd +++ b/L2J_Mobius_10.1_MasterClass/dist/game/data/xsd/npcs.xsd @@ -1,16 +1,5 @@ - - - - - - - - - - - @@ -260,11 +249,81 @@ - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/L2J_Mobius_10.1_MasterClass/java/org/l2jmobius/gameserver/data/xml/NpcData.java b/L2J_Mobius_10.1_MasterClass/java/org/l2jmobius/gameserver/data/xml/NpcData.java index 0d537566a1..5286a49d63 100644 --- a/L2J_Mobius_10.1_MasterClass/java/org/l2jmobius/gameserver/data/xml/NpcData.java +++ b/L2J_Mobius_10.1_MasterClass/java/org/l2jmobius/gameserver/data/xml/NpcData.java @@ -46,6 +46,7 @@ import org.l2jmobius.gameserver.enums.MpRewardType; import org.l2jmobius.gameserver.model.StatSet; import org.l2jmobius.gameserver.model.actor.templates.NpcTemplate; import org.l2jmobius.gameserver.model.effects.EffectType; +import org.l2jmobius.gameserver.model.holders.DropGroupHolder; import org.l2jmobius.gameserver.model.holders.DropHolder; import org.l2jmobius.gameserver.model.skill.Skill; @@ -100,6 +101,7 @@ public class NpcData implements IXmlReader Set clans = null; Set ignoreClanNpcIds = null; List dropLists = null; + List dropGroups = null; set.set("id", npcId); set.set("displayId", parseInteger(attrs, "displayId")); set.set("level", parseInteger(attrs, "level")); @@ -436,24 +438,54 @@ public class NpcData implements IXmlReader if (dropType != null) { - if (dropLists == null) - { - dropLists = new ArrayList<>(); - } - for (Node dropNode = dropListsNode.getFirstChild(); dropNode != null; dropNode = dropNode.getNextSibling()) { - final NamedNodeMap dropAttrs = dropNode.getAttributes(); - if ("item".equalsIgnoreCase(dropNode.getNodeName())) + final String nodeName = dropNode.getNodeName(); + if (nodeName.equalsIgnoreCase("group")) { - final DropHolder dropItem = new DropHolder(dropType, parseInteger(dropAttrs, "id"), parseLong(dropAttrs, "min"), parseLong(dropAttrs, "max"), parseDouble(dropAttrs, "chance")); - if (ItemTable.getInstance().getTemplate(parseInteger(dropAttrs, "id")) == null) + if (dropGroups == null) { - LOGGER.warning("DropListItem: Could not find item with id " + parseInteger(dropAttrs, "id") + "."); + dropGroups = new ArrayList<>(); + } + + final DropGroupHolder group = new DropGroupHolder(parseDouble(dropNode.getAttributes(), "chance")); + for (Node groupNode = dropNode.getFirstChild(); groupNode != null; groupNode = groupNode.getNextSibling()) + { + if (groupNode.getNodeName().equalsIgnoreCase("item")) + { + final NamedNodeMap groupAttrs = groupNode.getAttributes(); + final int itemId = parseInteger(groupAttrs, "id"); + + if (ItemTable.getInstance().getTemplate(itemId) == null) + { + LOGGER.warning("DropListItem: Could not find item with id " + itemId + "."); + } + else + { + group.addDrop(new DropHolder(dropType, itemId, parseLong(groupAttrs, "min"), parseLong(groupAttrs, "max"), parseDouble(groupAttrs, "chance"))); + } + } + } + + dropGroups.add(group); + } + else if (nodeName.equalsIgnoreCase("item")) + { + if (dropLists == null) + { + dropLists = new ArrayList<>(); + } + + final NamedNodeMap dropAttrs = dropNode.getAttributes(); + final int itemId = parseInteger(dropAttrs, "id"); + + if (ItemTable.getInstance().getTemplate(itemId) == null) + { + LOGGER.warning("DropListItem: Could not find item with id " + itemId + "."); } else { - dropLists.add(dropItem); + dropLists.add(new DropHolder(dropType, itemId, parseLong(dropAttrs, "min"), parseLong(dropAttrs, "max"), parseDouble(dropAttrs, "chance"))); } } } @@ -612,10 +644,21 @@ public class NpcData implements IXmlReader template.setClans(clans); template.setIgnoreClanNpcIds(ignoreClanNpcIds); + // Clean old drop groups. + template.removeDropGroups(); + + // Set new drop groups. + if (dropGroups != null) + { + template.setDropGroups(dropGroups); + } + + // Clean old drop lists. + template.removeDrops(); + + // Set new drop lists. if (dropLists != null) { - template.removeDrops(); - // Drops are sorted by chance (high to low). Collections.sort(dropLists, (d1, d2) -> Double.valueOf(d2.getChance()).compareTo(Double.valueOf(d1.getChance()))); for (DropHolder dropHolder : dropLists) diff --git a/L2J_Mobius_10.1_MasterClass/java/org/l2jmobius/gameserver/model/actor/templates/NpcTemplate.java b/L2J_Mobius_10.1_MasterClass/java/org/l2jmobius/gameserver/model/actor/templates/NpcTemplate.java index 05af374536..5237927054 100644 --- a/L2J_Mobius_10.1_MasterClass/java/org/l2jmobius/gameserver/model/actor/templates/NpcTemplate.java +++ b/L2J_Mobius_10.1_MasterClass/java/org/l2jmobius/gameserver/model/actor/templates/NpcTemplate.java @@ -36,6 +36,7 @@ import org.l2jmobius.gameserver.enums.Race; import org.l2jmobius.gameserver.enums.Sex; import org.l2jmobius.gameserver.model.StatSet; import org.l2jmobius.gameserver.model.actor.Creature; +import org.l2jmobius.gameserver.model.holders.DropGroupHolder; import org.l2jmobius.gameserver.model.holders.DropHolder; import org.l2jmobius.gameserver.model.holders.ItemHolder; import org.l2jmobius.gameserver.model.interfaces.IIdentifiable; @@ -107,6 +108,7 @@ public class NpcTemplate extends CreatureTemplate implements IIdentifiable private Map> _aiSkillLists; private Set _clans; private Set _ignoreClanNpcIds; + private List _dropGroups; private List _dropListDeath; private List _dropListSpoil; private float _collisionRadiusGrown; @@ -649,12 +651,22 @@ public class NpcTemplate extends CreatureTemplate implements IIdentifiable _ignoreClanNpcIds = ignoreClanNpcIds != null ? Collections.unmodifiableSet(ignoreClanNpcIds) : null; } + public void removeDropGroups() + { + _dropGroups = null; + } + public void removeDrops() { _dropListDeath = null; _dropListSpoil = null; } + public void setDropGroups(List groups) + { + _dropGroups = groups; + } + public void addDrop(DropHolder dropHolder) { if (_dropListDeath == null) @@ -673,6 +685,11 @@ public class NpcTemplate extends CreatureTemplate implements IIdentifiable _dropListSpoil.add(dropHolder); } + public List getDropGroups() + { + return _dropGroups; + } + public List getDropList() { return _dropListDeath; @@ -685,11 +702,201 @@ public class NpcTemplate extends CreatureTemplate implements IIdentifiable public List calculateDrops(DropType dropType, Creature victim, Creature killer) { - final List dropList = dropType == DropType.SPOIL ? _dropListSpoil : _dropListDeath; - if (dropList == null) + if (dropType == DropType.DROP) { - return null; + // calculate group drops + List groupDrops = null; + if (_dropGroups != null) + { + groupDrops = calculateGroupDrops(victim, killer); + } + + // calculate ungrouped drops + List ungroupedDrops = null; + if (_dropListDeath != null) + { + ungroupedDrops = calculateUngroupedDrops(dropType, victim, killer); + } + + // return results + if ((groupDrops != null) && (ungroupedDrops != null)) + { + groupDrops.addAll(ungroupedDrops); + ungroupedDrops.clear(); + return groupDrops; + } + if (groupDrops != null) + { + return groupDrops; + } + if (ungroupedDrops != null) + { + return ungroupedDrops; + } } + else if ((dropType == DropType.SPOIL) && (_dropListSpoil != null)) + { + return calculateUngroupedDrops(dropType, victim, killer); + } + + // no drops + return null; + } + + private List calculateGroupDrops(Creature victim, Creature killer) + { + // level difference calculations + final int levelDifference = victim.getLevel() - killer.getLevel(); + final double levelGapChanceToDropAdena = Util.map(levelDifference, -Config.DROP_ADENA_MAX_LEVEL_DIFFERENCE, -Config.DROP_ADENA_MIN_LEVEL_DIFFERENCE, Config.DROP_ADENA_MIN_LEVEL_GAP_CHANCE, 100d); + final double levelGapChanceToDrop = Util.map(levelDifference, -Config.DROP_ITEM_MAX_LEVEL_DIFFERENCE, -Config.DROP_ITEM_MIN_LEVEL_DIFFERENCE, Config.DROP_ITEM_MIN_LEVEL_GAP_CHANCE, 100d); + + int dropOccurrenceCounter = victim.isRaid() ? Config.DROP_MAX_OCCURRENCES_RAIDBOSS : Config.DROP_MAX_OCCURRENCES_NORMAL; + List calculatedDrops = null; + for (DropGroupHolder group : _dropGroups) + { + if (dropOccurrenceCounter <= 0) + { + break; + } + + double groupRate = 1; + final List groupDrops = group.getDropList(); + for (DropHolder dropItem : groupDrops) + { + final int itemId = dropItem.getItemId(); + final ItemTemplate item = ItemTable.getInstance().getTemplate(itemId); + final boolean champion = victim.isChampion(); + + // chance + double rateChance = 1; + final Float itemChance = Config.RATE_DROP_CHANCE_BY_ID.get(itemId); + if (itemChance != null) + { + if (itemChance == 0) + { + continue; + } + + rateChance *= itemChance; + if (champion && (itemId == Inventory.ADENA_ID)) + { + rateChance *= Config.CHAMPION_ADENAS_REWARDS_CHANCE; + } + } + else if (item.hasExImmediateEffect()) + { + rateChance *= Config.RATE_HERB_DROP_CHANCE_MULTIPLIER; + } + else if (victim.isRaid()) + { + rateChance *= Config.RATE_RAID_DROP_CHANCE_MULTIPLIER; + } + else + { + rateChance *= Config.RATE_DEATH_DROP_CHANCE_MULTIPLIER * (champion ? Config.CHAMPION_REWARDS_CHANCE : 1); + } + + // premium chance + if (Config.PREMIUM_SYSTEM_ENABLED && (killer.getActingPlayer() != null) && killer.getActingPlayer().hasPremiumStatus()) + { + if (Config.PREMIUM_RATE_DROP_CHANCE_BY_ID.get(itemId) != null) + { + rateChance *= Config.PREMIUM_RATE_DROP_CHANCE_BY_ID.get(itemId); + } + else if (item.hasExImmediateEffect()) + { + // TODO: Premium herb chance? :) + } + else if (victim.isRaid()) + { + // TODO: Premium raid chance? :) + } + else + { + rateChance *= Config.PREMIUM_RATE_DROP_CHANCE; + } + } + + // keep lowest to avoid chance by id configuration conflicts + groupRate = Math.min(groupRate, rateChance); + } + + if ((Rnd.nextDouble() * 100) < (group.getChance() * groupRate)) + { + double totalChance = 0; // total group chance is 100 + final double dropChance = Rnd.nextDouble() * 100; + GROUP_DROP: for (DropHolder dropItem : groupDrops) + { + if (dropOccurrenceCounter <= 0) + { + break; + } + + // calculate if item will drop + totalChance += dropItem.getChance(); + if (dropChance >= totalChance) + { + continue; + } + + // check level gap that may prevent to drop item + if ((Rnd.nextDouble() * 100) > (dropItem.getItemId() == Inventory.ADENA_ID ? levelGapChanceToDropAdena : levelGapChanceToDrop)) + { + continue; + } + + // create the drop + final ItemHolder drop = calculateGroupDrop(dropItem, victim, killer); + + // create list + if (calculatedDrops == null) + { + calculatedDrops = new ArrayList<>(dropOccurrenceCounter); + } + + // finally + if (group.getChance() < 100) + { + dropOccurrenceCounter--; + } + calculatedDrops.add(drop); + + // no more drops from this group + break GROUP_DROP; + } + } + } + + // champion extra drop + if (victim.isChampion()) + { + if ((victim.getLevel() < killer.getLevel()) && (Rnd.get(100) < Config.CHAMPION_REWARD_LOWER_LEVEL_ITEM_CHANCE)) + { + return calculatedDrops; + } + if ((victim.getLevel() > killer.getLevel()) && (Rnd.get(100) < Config.CHAMPION_REWARD_HIGHER_LEVEL_ITEM_CHANCE)) + { + return calculatedDrops; + } + + // create list + if (calculatedDrops == null) + { + calculatedDrops = new ArrayList<>(); + } + + if (!calculatedDrops.containsAll(Config.CHAMPION_REWARD_ITEMS)) + { + calculatedDrops.addAll(Config.CHAMPION_REWARD_ITEMS); + } + } + + return calculatedDrops; + } + + private List calculateUngroupedDrops(DropType dropType, Creature victim, Creature killer) + { + final List dropList = dropType == DropType.SPOIL ? _dropListSpoil : _dropListDeath; // level difference calculations final int levelDifference = victim.getLevel() - killer.getLevel(); @@ -721,7 +928,7 @@ public class NpcTemplate extends CreatureTemplate implements IIdentifiable } // calculate chances - final ItemHolder drop = calculateDrop(dropItem, victim, killer); + final ItemHolder drop = calculateUngroupedDrop(dropItem, victim, killer); if (drop == null) { continue; @@ -776,20 +983,82 @@ public class NpcTemplate extends CreatureTemplate implements IIdentifiable calculatedDrops = new ArrayList<>(); } - calculatedDrops.addAll(Config.CHAMPION_REWARD_ITEMS); + if (!calculatedDrops.containsAll(Config.CHAMPION_REWARD_ITEMS)) + { + calculatedDrops.addAll(Config.CHAMPION_REWARD_ITEMS); + } } return calculatedDrops; } /** - * All item drop chance calculations are done by this method. * @param dropItem * @param victim * @param killer * @return ItemHolder */ - private ItemHolder calculateDrop(DropHolder dropItem, Creature victim, Creature killer) + private ItemHolder calculateGroupDrop(DropHolder dropItem, Creature victim, Creature killer) + { + final int itemId = dropItem.getItemId(); + final ItemTemplate item = ItemTable.getInstance().getTemplate(itemId); + final boolean champion = victim.isChampion(); + + // calculate amount + double rateAmount = 1; + if (Config.RATE_DROP_AMOUNT_BY_ID.get(itemId) != null) + { + rateAmount *= Config.RATE_DROP_AMOUNT_BY_ID.get(itemId); + if (champion && (itemId == Inventory.ADENA_ID)) + { + rateAmount *= Config.CHAMPION_ADENAS_REWARDS_AMOUNT; + } + } + else if (item.hasExImmediateEffect()) + { + rateAmount *= Config.RATE_HERB_DROP_AMOUNT_MULTIPLIER; + } + else if (victim.isRaid()) + { + rateAmount *= Config.RATE_RAID_DROP_AMOUNT_MULTIPLIER; + } + else + { + rateAmount *= Config.RATE_DEATH_DROP_AMOUNT_MULTIPLIER * (champion ? Config.CHAMPION_REWARDS_AMOUNT : 1); + } + + // premium amount + if (Config.PREMIUM_SYSTEM_ENABLED && (killer.getActingPlayer() != null) && killer.getActingPlayer().hasPremiumStatus()) + { + if (Config.PREMIUM_RATE_DROP_AMOUNT_BY_ID.get(itemId) != null) + { + rateAmount *= Config.PREMIUM_RATE_DROP_AMOUNT_BY_ID.get(itemId); + } + else if (item.hasExImmediateEffect()) + { + // TODO: Premium herb amount? :) + } + else if (victim.isRaid()) + { + // TODO: Premium raid amount? :) + } + else + { + rateAmount *= Config.PREMIUM_RATE_DROP_AMOUNT; + } + } + + // finally + return new ItemHolder(itemId, (long) (Rnd.get(dropItem.getMin(), dropItem.getMax()) * rateAmount)); + } + + /** + * @param dropItem + * @param victim + * @param killer + * @return ItemHolder + */ + private ItemHolder calculateUngroupedDrop(DropHolder dropItem, Creature victim, Creature killer) { switch (dropItem.getDropType()) { diff --git a/L2J_Mobius_10.1_MasterClass/java/org/l2jmobius/gameserver/model/holders/DropGroupHolder.java b/L2J_Mobius_10.1_MasterClass/java/org/l2jmobius/gameserver/model/holders/DropGroupHolder.java new file mode 100644 index 0000000000..6c2401c0b0 --- /dev/null +++ b/L2J_Mobius_10.1_MasterClass/java/org/l2jmobius/gameserver/model/holders/DropGroupHolder.java @@ -0,0 +1,49 @@ +/* + * This file is part of the L2J Mobius project. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package org.l2jmobius.gameserver.model.holders; + +import java.util.ArrayList; +import java.util.List; + +/** + * @author Mobius + */ +public class DropGroupHolder +{ + private final List _dropList = new ArrayList<>(); + private final double _chance; + + public DropGroupHolder(double chance) + { + _chance = chance; + } + + public List getDropList() + { + return _dropList; + } + + public void addDrop(DropHolder holder) + { + _dropList.add(holder); + } + + public double getChance() + { + return _chance; + } +} diff --git a/L2J_Mobius_10.2_MasterClass/dist/game/data/scripts/handlers/bypasshandlers/NpcViewMod.java b/L2J_Mobius_10.2_MasterClass/dist/game/data/scripts/handlers/bypasshandlers/NpcViewMod.java index 35d31315ad..fab79abca8 100644 --- a/L2J_Mobius_10.2_MasterClass/dist/game/data/scripts/handlers/bypasshandlers/NpcViewMod.java +++ b/L2J_Mobius_10.2_MasterClass/dist/game/data/scripts/handlers/bypasshandlers/NpcViewMod.java @@ -37,6 +37,7 @@ import org.l2jmobius.gameserver.model.actor.Attackable; import org.l2jmobius.gameserver.model.actor.Creature; import org.l2jmobius.gameserver.model.actor.Npc; import org.l2jmobius.gameserver.model.actor.Player; +import org.l2jmobius.gameserver.model.holders.DropGroupHolder; import org.l2jmobius.gameserver.model.holders.DropHolder; import org.l2jmobius.gameserver.model.item.ItemTemplate; import org.l2jmobius.gameserver.model.itemcontainer.Inventory; @@ -326,12 +327,13 @@ public class NpcViewMod implements IBypassHandler private static String getDropListButtons(Npc npc) { final StringBuilder sb = new StringBuilder(); + final List dropListGroups = npc.getTemplate().getDropGroups(); final List dropListDeath = npc.getTemplate().getDropList(); final List dropListSpoil = npc.getTemplate().getSpoilList(); - if ((dropListDeath != null) || (dropListSpoil != null)) + if ((dropListGroups != null) || (dropListDeath != null) || (dropListSpoil != null)) { sb.append("
"); - if (dropListDeath != null) + if ((dropListGroups != null) || (dropListDeath != null)) { sb.append(""); } @@ -348,13 +350,40 @@ public class NpcViewMod implements IBypassHandler private void sendNpcDropList(Player player, Npc npc, DropType dropType, int pageValue) { - final List templateList = dropType == DropType.SPOIL ? npc.getTemplate().getSpoilList() : npc.getTemplate().getDropList(); - if (templateList == null) + List dropList = null; + if (dropType == DropType.SPOIL) + { + dropList = new ArrayList<>(npc.getTemplate().getSpoilList()); + } + else + { + final List drops = npc.getTemplate().getDropList(); + if (drops != null) + { + dropList = new ArrayList<>(drops); + } + final List dropGroups = npc.getTemplate().getDropGroups(); + if (dropGroups != null) + { + if (dropList == null) + { + dropList = new ArrayList<>(); + } + for (DropGroupHolder dropGroup : dropGroups) + { + final double chance = dropGroup.getChance() / 100; + for (DropHolder dropHolder : dropGroup.getDropList()) + { + dropList.add(new DropHolder(dropHolder.getDropType(), dropHolder.getItemId(), dropHolder.getMin(), dropHolder.getMax(), dropHolder.getChance() * chance)); + } + } + } + } + if (dropList == null) { return; } - final List dropList = new ArrayList<>(templateList); Collections.sort(dropList, (d1, d2) -> Integer.valueOf(d1.getItemId()).compareTo(Integer.valueOf(d2.getItemId()))); int pages = dropList.size() / DROP_LIST_ITEMS_PER_PAGE; diff --git a/L2J_Mobius_10.2_MasterClass/dist/game/data/scripts/handlers/communityboard/DropSearchBoard.java b/L2J_Mobius_10.2_MasterClass/dist/game/data/scripts/handlers/communityboard/DropSearchBoard.java index 8ab335bc8b..94af43751f 100644 --- a/L2J_Mobius_10.2_MasterClass/dist/game/data/scripts/handlers/communityboard/DropSearchBoard.java +++ b/L2J_Mobius_10.2_MasterClass/dist/game/data/scripts/handlers/communityboard/DropSearchBoard.java @@ -36,6 +36,7 @@ import org.l2jmobius.gameserver.handler.CommunityBoardHandler; import org.l2jmobius.gameserver.handler.IParseBoardHandler; import org.l2jmobius.gameserver.model.actor.Player; import org.l2jmobius.gameserver.model.actor.templates.NpcTemplate; +import org.l2jmobius.gameserver.model.holders.DropGroupHolder; import org.l2jmobius.gameserver.model.holders.DropHolder; import org.l2jmobius.gameserver.model.item.ItemTemplate; import org.l2jmobius.gameserver.model.itemcontainer.Inventory; @@ -104,6 +105,17 @@ public class DropSearchBoard implements IParseBoardHandler private void buildDropIndex() { + NpcData.getInstance().getTemplates(npc -> npc.getDropGroups() != null).forEach(npcTemplate -> + { + for (DropGroupHolder dropGroup : npcTemplate.getDropGroups()) + { + final double chance = dropGroup.getChance() / 100; + for (DropHolder dropHolder : dropGroup.getDropList()) + { + addToDropList(npcTemplate, new DropHolder(dropHolder.getDropType(), dropHolder.getItemId(), dropHolder.getMin(), dropHolder.getMax(), dropHolder.getChance() * chance)); + } + } + }); NpcData.getInstance().getTemplates(npc -> npc.getDropList() != null).forEach(npcTemplate -> { for (DropHolder dropHolder : npcTemplate.getDropList()) diff --git a/L2J_Mobius_10.2_MasterClass/dist/game/data/xsd/npcs.xsd b/L2J_Mobius_10.2_MasterClass/dist/game/data/xsd/npcs.xsd index 70611293a3..9068efd10b 100644 --- a/L2J_Mobius_10.2_MasterClass/dist/game/data/xsd/npcs.xsd +++ b/L2J_Mobius_10.2_MasterClass/dist/game/data/xsd/npcs.xsd @@ -1,16 +1,5 @@ - - - - - - - - - - - @@ -260,11 +249,81 @@ - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/L2J_Mobius_10.2_MasterClass/java/org/l2jmobius/gameserver/data/xml/NpcData.java b/L2J_Mobius_10.2_MasterClass/java/org/l2jmobius/gameserver/data/xml/NpcData.java index 0d537566a1..5286a49d63 100644 --- a/L2J_Mobius_10.2_MasterClass/java/org/l2jmobius/gameserver/data/xml/NpcData.java +++ b/L2J_Mobius_10.2_MasterClass/java/org/l2jmobius/gameserver/data/xml/NpcData.java @@ -46,6 +46,7 @@ import org.l2jmobius.gameserver.enums.MpRewardType; import org.l2jmobius.gameserver.model.StatSet; import org.l2jmobius.gameserver.model.actor.templates.NpcTemplate; import org.l2jmobius.gameserver.model.effects.EffectType; +import org.l2jmobius.gameserver.model.holders.DropGroupHolder; import org.l2jmobius.gameserver.model.holders.DropHolder; import org.l2jmobius.gameserver.model.skill.Skill; @@ -100,6 +101,7 @@ public class NpcData implements IXmlReader Set clans = null; Set ignoreClanNpcIds = null; List dropLists = null; + List dropGroups = null; set.set("id", npcId); set.set("displayId", parseInteger(attrs, "displayId")); set.set("level", parseInteger(attrs, "level")); @@ -436,24 +438,54 @@ public class NpcData implements IXmlReader if (dropType != null) { - if (dropLists == null) - { - dropLists = new ArrayList<>(); - } - for (Node dropNode = dropListsNode.getFirstChild(); dropNode != null; dropNode = dropNode.getNextSibling()) { - final NamedNodeMap dropAttrs = dropNode.getAttributes(); - if ("item".equalsIgnoreCase(dropNode.getNodeName())) + final String nodeName = dropNode.getNodeName(); + if (nodeName.equalsIgnoreCase("group")) { - final DropHolder dropItem = new DropHolder(dropType, parseInteger(dropAttrs, "id"), parseLong(dropAttrs, "min"), parseLong(dropAttrs, "max"), parseDouble(dropAttrs, "chance")); - if (ItemTable.getInstance().getTemplate(parseInteger(dropAttrs, "id")) == null) + if (dropGroups == null) { - LOGGER.warning("DropListItem: Could not find item with id " + parseInteger(dropAttrs, "id") + "."); + dropGroups = new ArrayList<>(); + } + + final DropGroupHolder group = new DropGroupHolder(parseDouble(dropNode.getAttributes(), "chance")); + for (Node groupNode = dropNode.getFirstChild(); groupNode != null; groupNode = groupNode.getNextSibling()) + { + if (groupNode.getNodeName().equalsIgnoreCase("item")) + { + final NamedNodeMap groupAttrs = groupNode.getAttributes(); + final int itemId = parseInteger(groupAttrs, "id"); + + if (ItemTable.getInstance().getTemplate(itemId) == null) + { + LOGGER.warning("DropListItem: Could not find item with id " + itemId + "."); + } + else + { + group.addDrop(new DropHolder(dropType, itemId, parseLong(groupAttrs, "min"), parseLong(groupAttrs, "max"), parseDouble(groupAttrs, "chance"))); + } + } + } + + dropGroups.add(group); + } + else if (nodeName.equalsIgnoreCase("item")) + { + if (dropLists == null) + { + dropLists = new ArrayList<>(); + } + + final NamedNodeMap dropAttrs = dropNode.getAttributes(); + final int itemId = parseInteger(dropAttrs, "id"); + + if (ItemTable.getInstance().getTemplate(itemId) == null) + { + LOGGER.warning("DropListItem: Could not find item with id " + itemId + "."); } else { - dropLists.add(dropItem); + dropLists.add(new DropHolder(dropType, itemId, parseLong(dropAttrs, "min"), parseLong(dropAttrs, "max"), parseDouble(dropAttrs, "chance"))); } } } @@ -612,10 +644,21 @@ public class NpcData implements IXmlReader template.setClans(clans); template.setIgnoreClanNpcIds(ignoreClanNpcIds); + // Clean old drop groups. + template.removeDropGroups(); + + // Set new drop groups. + if (dropGroups != null) + { + template.setDropGroups(dropGroups); + } + + // Clean old drop lists. + template.removeDrops(); + + // Set new drop lists. if (dropLists != null) { - template.removeDrops(); - // Drops are sorted by chance (high to low). Collections.sort(dropLists, (d1, d2) -> Double.valueOf(d2.getChance()).compareTo(Double.valueOf(d1.getChance()))); for (DropHolder dropHolder : dropLists) diff --git a/L2J_Mobius_10.2_MasterClass/java/org/l2jmobius/gameserver/model/actor/templates/NpcTemplate.java b/L2J_Mobius_10.2_MasterClass/java/org/l2jmobius/gameserver/model/actor/templates/NpcTemplate.java index 05af374536..5237927054 100644 --- a/L2J_Mobius_10.2_MasterClass/java/org/l2jmobius/gameserver/model/actor/templates/NpcTemplate.java +++ b/L2J_Mobius_10.2_MasterClass/java/org/l2jmobius/gameserver/model/actor/templates/NpcTemplate.java @@ -36,6 +36,7 @@ import org.l2jmobius.gameserver.enums.Race; import org.l2jmobius.gameserver.enums.Sex; import org.l2jmobius.gameserver.model.StatSet; import org.l2jmobius.gameserver.model.actor.Creature; +import org.l2jmobius.gameserver.model.holders.DropGroupHolder; import org.l2jmobius.gameserver.model.holders.DropHolder; import org.l2jmobius.gameserver.model.holders.ItemHolder; import org.l2jmobius.gameserver.model.interfaces.IIdentifiable; @@ -107,6 +108,7 @@ public class NpcTemplate extends CreatureTemplate implements IIdentifiable private Map> _aiSkillLists; private Set _clans; private Set _ignoreClanNpcIds; + private List _dropGroups; private List _dropListDeath; private List _dropListSpoil; private float _collisionRadiusGrown; @@ -649,12 +651,22 @@ public class NpcTemplate extends CreatureTemplate implements IIdentifiable _ignoreClanNpcIds = ignoreClanNpcIds != null ? Collections.unmodifiableSet(ignoreClanNpcIds) : null; } + public void removeDropGroups() + { + _dropGroups = null; + } + public void removeDrops() { _dropListDeath = null; _dropListSpoil = null; } + public void setDropGroups(List groups) + { + _dropGroups = groups; + } + public void addDrop(DropHolder dropHolder) { if (_dropListDeath == null) @@ -673,6 +685,11 @@ public class NpcTemplate extends CreatureTemplate implements IIdentifiable _dropListSpoil.add(dropHolder); } + public List getDropGroups() + { + return _dropGroups; + } + public List getDropList() { return _dropListDeath; @@ -685,11 +702,201 @@ public class NpcTemplate extends CreatureTemplate implements IIdentifiable public List calculateDrops(DropType dropType, Creature victim, Creature killer) { - final List dropList = dropType == DropType.SPOIL ? _dropListSpoil : _dropListDeath; - if (dropList == null) + if (dropType == DropType.DROP) { - return null; + // calculate group drops + List groupDrops = null; + if (_dropGroups != null) + { + groupDrops = calculateGroupDrops(victim, killer); + } + + // calculate ungrouped drops + List ungroupedDrops = null; + if (_dropListDeath != null) + { + ungroupedDrops = calculateUngroupedDrops(dropType, victim, killer); + } + + // return results + if ((groupDrops != null) && (ungroupedDrops != null)) + { + groupDrops.addAll(ungroupedDrops); + ungroupedDrops.clear(); + return groupDrops; + } + if (groupDrops != null) + { + return groupDrops; + } + if (ungroupedDrops != null) + { + return ungroupedDrops; + } } + else if ((dropType == DropType.SPOIL) && (_dropListSpoil != null)) + { + return calculateUngroupedDrops(dropType, victim, killer); + } + + // no drops + return null; + } + + private List calculateGroupDrops(Creature victim, Creature killer) + { + // level difference calculations + final int levelDifference = victim.getLevel() - killer.getLevel(); + final double levelGapChanceToDropAdena = Util.map(levelDifference, -Config.DROP_ADENA_MAX_LEVEL_DIFFERENCE, -Config.DROP_ADENA_MIN_LEVEL_DIFFERENCE, Config.DROP_ADENA_MIN_LEVEL_GAP_CHANCE, 100d); + final double levelGapChanceToDrop = Util.map(levelDifference, -Config.DROP_ITEM_MAX_LEVEL_DIFFERENCE, -Config.DROP_ITEM_MIN_LEVEL_DIFFERENCE, Config.DROP_ITEM_MIN_LEVEL_GAP_CHANCE, 100d); + + int dropOccurrenceCounter = victim.isRaid() ? Config.DROP_MAX_OCCURRENCES_RAIDBOSS : Config.DROP_MAX_OCCURRENCES_NORMAL; + List calculatedDrops = null; + for (DropGroupHolder group : _dropGroups) + { + if (dropOccurrenceCounter <= 0) + { + break; + } + + double groupRate = 1; + final List groupDrops = group.getDropList(); + for (DropHolder dropItem : groupDrops) + { + final int itemId = dropItem.getItemId(); + final ItemTemplate item = ItemTable.getInstance().getTemplate(itemId); + final boolean champion = victim.isChampion(); + + // chance + double rateChance = 1; + final Float itemChance = Config.RATE_DROP_CHANCE_BY_ID.get(itemId); + if (itemChance != null) + { + if (itemChance == 0) + { + continue; + } + + rateChance *= itemChance; + if (champion && (itemId == Inventory.ADENA_ID)) + { + rateChance *= Config.CHAMPION_ADENAS_REWARDS_CHANCE; + } + } + else if (item.hasExImmediateEffect()) + { + rateChance *= Config.RATE_HERB_DROP_CHANCE_MULTIPLIER; + } + else if (victim.isRaid()) + { + rateChance *= Config.RATE_RAID_DROP_CHANCE_MULTIPLIER; + } + else + { + rateChance *= Config.RATE_DEATH_DROP_CHANCE_MULTIPLIER * (champion ? Config.CHAMPION_REWARDS_CHANCE : 1); + } + + // premium chance + if (Config.PREMIUM_SYSTEM_ENABLED && (killer.getActingPlayer() != null) && killer.getActingPlayer().hasPremiumStatus()) + { + if (Config.PREMIUM_RATE_DROP_CHANCE_BY_ID.get(itemId) != null) + { + rateChance *= Config.PREMIUM_RATE_DROP_CHANCE_BY_ID.get(itemId); + } + else if (item.hasExImmediateEffect()) + { + // TODO: Premium herb chance? :) + } + else if (victim.isRaid()) + { + // TODO: Premium raid chance? :) + } + else + { + rateChance *= Config.PREMIUM_RATE_DROP_CHANCE; + } + } + + // keep lowest to avoid chance by id configuration conflicts + groupRate = Math.min(groupRate, rateChance); + } + + if ((Rnd.nextDouble() * 100) < (group.getChance() * groupRate)) + { + double totalChance = 0; // total group chance is 100 + final double dropChance = Rnd.nextDouble() * 100; + GROUP_DROP: for (DropHolder dropItem : groupDrops) + { + if (dropOccurrenceCounter <= 0) + { + break; + } + + // calculate if item will drop + totalChance += dropItem.getChance(); + if (dropChance >= totalChance) + { + continue; + } + + // check level gap that may prevent to drop item + if ((Rnd.nextDouble() * 100) > (dropItem.getItemId() == Inventory.ADENA_ID ? levelGapChanceToDropAdena : levelGapChanceToDrop)) + { + continue; + } + + // create the drop + final ItemHolder drop = calculateGroupDrop(dropItem, victim, killer); + + // create list + if (calculatedDrops == null) + { + calculatedDrops = new ArrayList<>(dropOccurrenceCounter); + } + + // finally + if (group.getChance() < 100) + { + dropOccurrenceCounter--; + } + calculatedDrops.add(drop); + + // no more drops from this group + break GROUP_DROP; + } + } + } + + // champion extra drop + if (victim.isChampion()) + { + if ((victim.getLevel() < killer.getLevel()) && (Rnd.get(100) < Config.CHAMPION_REWARD_LOWER_LEVEL_ITEM_CHANCE)) + { + return calculatedDrops; + } + if ((victim.getLevel() > killer.getLevel()) && (Rnd.get(100) < Config.CHAMPION_REWARD_HIGHER_LEVEL_ITEM_CHANCE)) + { + return calculatedDrops; + } + + // create list + if (calculatedDrops == null) + { + calculatedDrops = new ArrayList<>(); + } + + if (!calculatedDrops.containsAll(Config.CHAMPION_REWARD_ITEMS)) + { + calculatedDrops.addAll(Config.CHAMPION_REWARD_ITEMS); + } + } + + return calculatedDrops; + } + + private List calculateUngroupedDrops(DropType dropType, Creature victim, Creature killer) + { + final List dropList = dropType == DropType.SPOIL ? _dropListSpoil : _dropListDeath; // level difference calculations final int levelDifference = victim.getLevel() - killer.getLevel(); @@ -721,7 +928,7 @@ public class NpcTemplate extends CreatureTemplate implements IIdentifiable } // calculate chances - final ItemHolder drop = calculateDrop(dropItem, victim, killer); + final ItemHolder drop = calculateUngroupedDrop(dropItem, victim, killer); if (drop == null) { continue; @@ -776,20 +983,82 @@ public class NpcTemplate extends CreatureTemplate implements IIdentifiable calculatedDrops = new ArrayList<>(); } - calculatedDrops.addAll(Config.CHAMPION_REWARD_ITEMS); + if (!calculatedDrops.containsAll(Config.CHAMPION_REWARD_ITEMS)) + { + calculatedDrops.addAll(Config.CHAMPION_REWARD_ITEMS); + } } return calculatedDrops; } /** - * All item drop chance calculations are done by this method. * @param dropItem * @param victim * @param killer * @return ItemHolder */ - private ItemHolder calculateDrop(DropHolder dropItem, Creature victim, Creature killer) + private ItemHolder calculateGroupDrop(DropHolder dropItem, Creature victim, Creature killer) + { + final int itemId = dropItem.getItemId(); + final ItemTemplate item = ItemTable.getInstance().getTemplate(itemId); + final boolean champion = victim.isChampion(); + + // calculate amount + double rateAmount = 1; + if (Config.RATE_DROP_AMOUNT_BY_ID.get(itemId) != null) + { + rateAmount *= Config.RATE_DROP_AMOUNT_BY_ID.get(itemId); + if (champion && (itemId == Inventory.ADENA_ID)) + { + rateAmount *= Config.CHAMPION_ADENAS_REWARDS_AMOUNT; + } + } + else if (item.hasExImmediateEffect()) + { + rateAmount *= Config.RATE_HERB_DROP_AMOUNT_MULTIPLIER; + } + else if (victim.isRaid()) + { + rateAmount *= Config.RATE_RAID_DROP_AMOUNT_MULTIPLIER; + } + else + { + rateAmount *= Config.RATE_DEATH_DROP_AMOUNT_MULTIPLIER * (champion ? Config.CHAMPION_REWARDS_AMOUNT : 1); + } + + // premium amount + if (Config.PREMIUM_SYSTEM_ENABLED && (killer.getActingPlayer() != null) && killer.getActingPlayer().hasPremiumStatus()) + { + if (Config.PREMIUM_RATE_DROP_AMOUNT_BY_ID.get(itemId) != null) + { + rateAmount *= Config.PREMIUM_RATE_DROP_AMOUNT_BY_ID.get(itemId); + } + else if (item.hasExImmediateEffect()) + { + // TODO: Premium herb amount? :) + } + else if (victim.isRaid()) + { + // TODO: Premium raid amount? :) + } + else + { + rateAmount *= Config.PREMIUM_RATE_DROP_AMOUNT; + } + } + + // finally + return new ItemHolder(itemId, (long) (Rnd.get(dropItem.getMin(), dropItem.getMax()) * rateAmount)); + } + + /** + * @param dropItem + * @param victim + * @param killer + * @return ItemHolder + */ + private ItemHolder calculateUngroupedDrop(DropHolder dropItem, Creature victim, Creature killer) { switch (dropItem.getDropType()) { diff --git a/L2J_Mobius_10.2_MasterClass/java/org/l2jmobius/gameserver/model/holders/DropGroupHolder.java b/L2J_Mobius_10.2_MasterClass/java/org/l2jmobius/gameserver/model/holders/DropGroupHolder.java new file mode 100644 index 0000000000..6c2401c0b0 --- /dev/null +++ b/L2J_Mobius_10.2_MasterClass/java/org/l2jmobius/gameserver/model/holders/DropGroupHolder.java @@ -0,0 +1,49 @@ +/* + * This file is part of the L2J Mobius project. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package org.l2jmobius.gameserver.model.holders; + +import java.util.ArrayList; +import java.util.List; + +/** + * @author Mobius + */ +public class DropGroupHolder +{ + private final List _dropList = new ArrayList<>(); + private final double _chance; + + public DropGroupHolder(double chance) + { + _chance = chance; + } + + public List getDropList() + { + return _dropList; + } + + public void addDrop(DropHolder holder) + { + _dropList.add(holder); + } + + public double getChance() + { + return _chance; + } +} diff --git a/L2J_Mobius_CT_2.4_Epilogue/dist/game/data/scripts/handlers/bypasshandlers/NpcViewMod.java b/L2J_Mobius_CT_2.4_Epilogue/dist/game/data/scripts/handlers/bypasshandlers/NpcViewMod.java index c51448b987..1b4d35e38e 100644 --- a/L2J_Mobius_CT_2.4_Epilogue/dist/game/data/scripts/handlers/bypasshandlers/NpcViewMod.java +++ b/L2J_Mobius_CT_2.4_Epilogue/dist/game/data/scripts/handlers/bypasshandlers/NpcViewMod.java @@ -35,6 +35,7 @@ import org.l2jmobius.gameserver.model.WorldObject; import org.l2jmobius.gameserver.model.actor.Creature; import org.l2jmobius.gameserver.model.actor.Npc; import org.l2jmobius.gameserver.model.actor.Player; +import org.l2jmobius.gameserver.model.holders.DropGroupHolder; import org.l2jmobius.gameserver.model.holders.DropHolder; import org.l2jmobius.gameserver.model.item.ItemTemplate; import org.l2jmobius.gameserver.network.serverpackets.NpcHtmlMessage; @@ -206,12 +207,13 @@ public class NpcViewMod implements IBypassHandler private static String getDropListButtons(Npc npc) { final StringBuilder sb = new StringBuilder(); + final List dropListGroups = npc.getTemplate().getDropGroups(); final List dropListDeath = npc.getTemplate().getDropList(); final List dropListSpoil = npc.getTemplate().getSpoilList(); - if ((dropListDeath != null) || (dropListSpoil != null)) + if ((dropListGroups != null) || (dropListDeath != null) || (dropListSpoil != null)) { sb.append("
"); - if (dropListDeath != null) + if ((dropListGroups != null) || (dropListDeath != null)) { sb.append(""); } @@ -228,13 +230,40 @@ public class NpcViewMod implements IBypassHandler private void sendNpcDropList(Player player, Npc npc, DropType dropType, int pageValue) { - final List templateList = dropType == DropType.SPOIL ? npc.getTemplate().getSpoilList() : npc.getTemplate().getDropList(); - if (templateList == null) + List dropList = null; + if (dropType == DropType.SPOIL) + { + dropList = new ArrayList<>(npc.getTemplate().getSpoilList()); + } + else + { + final List drops = npc.getTemplate().getDropList(); + if (drops != null) + { + dropList = new ArrayList<>(drops); + } + final List dropGroups = npc.getTemplate().getDropGroups(); + if (dropGroups != null) + { + if (dropList == null) + { + dropList = new ArrayList<>(); + } + for (DropGroupHolder dropGroup : dropGroups) + { + final double chance = dropGroup.getChance() / 100; + for (DropHolder dropHolder : dropGroup.getDropList()) + { + dropList.add(new DropHolder(dropHolder.getDropType(), dropHolder.getItemId(), dropHolder.getMin(), dropHolder.getMax(), dropHolder.getChance() * chance)); + } + } + } + } + if (dropList == null) { return; } - final List dropList = new ArrayList<>(templateList); Collections.sort(dropList, (d1, d2) -> Integer.valueOf(d1.getItemId()).compareTo(Integer.valueOf(d2.getItemId()))); int pages = dropList.size() / DROP_LIST_ITEMS_PER_PAGE; diff --git a/L2J_Mobius_CT_2.4_Epilogue/dist/game/data/scripts/handlers/communityboard/DropSearchBoard.java b/L2J_Mobius_CT_2.4_Epilogue/dist/game/data/scripts/handlers/communityboard/DropSearchBoard.java index e2cef26f29..51f76f73fa 100644 --- a/L2J_Mobius_CT_2.4_Epilogue/dist/game/data/scripts/handlers/communityboard/DropSearchBoard.java +++ b/L2J_Mobius_CT_2.4_Epilogue/dist/game/data/scripts/handlers/communityboard/DropSearchBoard.java @@ -36,6 +36,7 @@ import org.l2jmobius.gameserver.handler.IParseBoardHandler; import org.l2jmobius.gameserver.model.Spawn; import org.l2jmobius.gameserver.model.actor.Player; import org.l2jmobius.gameserver.model.actor.templates.NpcTemplate; +import org.l2jmobius.gameserver.model.holders.DropGroupHolder; import org.l2jmobius.gameserver.model.holders.DropHolder; import org.l2jmobius.gameserver.model.item.ItemTemplate; import org.l2jmobius.gameserver.model.itemcontainer.Inventory; @@ -102,6 +103,17 @@ public class DropSearchBoard implements IParseBoardHandler private void buildDropIndex() { + NpcData.getInstance().getTemplates(npc -> npc.getDropGroups() != null).forEach(npcTemplate -> + { + for (DropGroupHolder dropGroup : npcTemplate.getDropGroups()) + { + final double chance = dropGroup.getChance() / 100; + for (DropHolder dropHolder : dropGroup.getDropList()) + { + addToDropList(npcTemplate, new DropHolder(dropHolder.getDropType(), dropHolder.getItemId(), dropHolder.getMin(), dropHolder.getMax(), dropHolder.getChance() * chance)); + } + } + }); NpcData.getInstance().getTemplates(npc -> npc.getDropList() != null).forEach(npcTemplate -> { for (DropHolder dropHolder : npcTemplate.getDropList()) diff --git a/L2J_Mobius_CT_2.4_Epilogue/dist/game/data/xsd/npcs.xsd b/L2J_Mobius_CT_2.4_Epilogue/dist/game/data/xsd/npcs.xsd index 54228dcc3e..43979e19b5 100644 --- a/L2J_Mobius_CT_2.4_Epilogue/dist/game/data/xsd/npcs.xsd +++ b/L2J_Mobius_CT_2.4_Epilogue/dist/game/data/xsd/npcs.xsd @@ -13,7 +13,67 @@ - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/L2J_Mobius_CT_2.4_Epilogue/java/org/l2jmobius/gameserver/data/xml/NpcData.java b/L2J_Mobius_CT_2.4_Epilogue/java/org/l2jmobius/gameserver/data/xml/NpcData.java index dbb14441c6..a0b90059fb 100644 --- a/L2J_Mobius_CT_2.4_Epilogue/java/org/l2jmobius/gameserver/data/xml/NpcData.java +++ b/L2J_Mobius_CT_2.4_Epilogue/java/org/l2jmobius/gameserver/data/xml/NpcData.java @@ -43,6 +43,7 @@ import org.l2jmobius.gameserver.enums.DropType; import org.l2jmobius.gameserver.model.StatSet; import org.l2jmobius.gameserver.model.actor.templates.NpcTemplate; import org.l2jmobius.gameserver.model.effects.EffectType; +import org.l2jmobius.gameserver.model.holders.DropGroupHolder; import org.l2jmobius.gameserver.model.holders.DropHolder; import org.l2jmobius.gameserver.model.holders.MinionHolder; import org.l2jmobius.gameserver.model.holders.SkillHolder; @@ -99,6 +100,7 @@ public class NpcData implements IXmlReader Set clans = null; Set ignoreClanNpcIds = null; List dropLists = null; + List dropGroups = null; set.set("id", npcId); set.set("displayId", parseInteger(attrs, "displayId")); set.set("level", parseByte(attrs, "level")); @@ -456,24 +458,54 @@ public class NpcData implements IXmlReader if (dropType != null) { - if (dropLists == null) - { - dropLists = new ArrayList<>(); - } - for (Node dropNode = dropListsNode.getFirstChild(); dropNode != null; dropNode = dropNode.getNextSibling()) { - final NamedNodeMap dropAttrs = dropNode.getAttributes(); - if ("item".equalsIgnoreCase(dropNode.getNodeName())) + final String nodeName = dropNode.getNodeName(); + if (nodeName.equalsIgnoreCase("group")) { - final DropHolder dropItem = new DropHolder(dropType, parseInteger(dropAttrs, "id"), parseLong(dropAttrs, "min"), parseLong(dropAttrs, "max"), parseDouble(dropAttrs, "chance")); - if (ItemTable.getInstance().getTemplate(parseInteger(dropAttrs, "id")) == null) + if (dropGroups == null) { - LOGGER.warning("DropListItem: Could not find item with id " + parseInteger(dropAttrs, "id") + "."); + dropGroups = new ArrayList<>(); + } + + final DropGroupHolder group = new DropGroupHolder(parseDouble(dropNode.getAttributes(), "chance")); + for (Node groupNode = dropNode.getFirstChild(); groupNode != null; groupNode = groupNode.getNextSibling()) + { + if (groupNode.getNodeName().equalsIgnoreCase("item")) + { + final NamedNodeMap groupAttrs = groupNode.getAttributes(); + final int itemId = parseInteger(groupAttrs, "id"); + + if (ItemTable.getInstance().getTemplate(itemId) == null) + { + LOGGER.warning("DropListItem: Could not find item with id " + itemId + "."); + } + else + { + group.addDrop(new DropHolder(dropType, itemId, parseLong(groupAttrs, "min"), parseLong(groupAttrs, "max"), parseDouble(groupAttrs, "chance"))); + } + } + } + + dropGroups.add(group); + } + else if (nodeName.equalsIgnoreCase("item")) + { + if (dropLists == null) + { + dropLists = new ArrayList<>(); + } + + final NamedNodeMap dropAttrs = dropNode.getAttributes(); + final int itemId = parseInteger(dropAttrs, "id"); + + if (ItemTable.getInstance().getTemplate(itemId) == null) + { + LOGGER.warning("DropListItem: Could not find item with id " + itemId + "."); } else { - dropLists.add(dropItem); + dropLists.add(new DropHolder(dropType, itemId, parseLong(dropAttrs, "min"), parseLong(dropAttrs, "max"), parseDouble(dropAttrs, "chance"))); } } } @@ -630,10 +662,21 @@ public class NpcData implements IXmlReader template.setClans(clans); template.setIgnoreClanNpcIds(ignoreClanNpcIds); + // Clean old drop groups. + template.removeDropGroups(); + + // Set new drop groups. + if (dropGroups != null) + { + template.setDropGroups(dropGroups); + } + + // Clean old drop lists. + template.removeDrops(); + + // Set new drop lists. if (dropLists != null) { - template.removeDrops(); - // Drops are sorted by chance (high to low). Collections.sort(dropLists, (d1, d2) -> Double.valueOf(d2.getChance()).compareTo(Double.valueOf(d1.getChance()))); for (DropHolder dropHolder : dropLists) diff --git a/L2J_Mobius_CT_2.4_Epilogue/java/org/l2jmobius/gameserver/model/actor/templates/NpcTemplate.java b/L2J_Mobius_CT_2.4_Epilogue/java/org/l2jmobius/gameserver/model/actor/templates/NpcTemplate.java index 3d4eded097..83fadfc4d7 100644 --- a/L2J_Mobius_CT_2.4_Epilogue/java/org/l2jmobius/gameserver/model/actor/templates/NpcTemplate.java +++ b/L2J_Mobius_CT_2.4_Epilogue/java/org/l2jmobius/gameserver/model/actor/templates/NpcTemplate.java @@ -34,6 +34,7 @@ import org.l2jmobius.gameserver.enums.Race; import org.l2jmobius.gameserver.enums.Sex; import org.l2jmobius.gameserver.model.StatSet; import org.l2jmobius.gameserver.model.actor.Creature; +import org.l2jmobius.gameserver.model.holders.DropGroupHolder; import org.l2jmobius.gameserver.model.holders.DropHolder; import org.l2jmobius.gameserver.model.holders.ItemHolder; import org.l2jmobius.gameserver.model.interfaces.IIdentifiable; @@ -98,6 +99,7 @@ public class NpcTemplate extends CreatureTemplate implements IIdentifiable private Map> _aiSkillLists; private Set _clans; private Set _ignoreClanNpcIds; + private List _dropGroups; private List _dropListDeath; private List _dropListSpoil; private double _collisionRadiusGrown; @@ -588,12 +590,22 @@ public class NpcTemplate extends CreatureTemplate implements IIdentifiable _ignoreClanNpcIds = ignoreClanNpcIds != null ? Collections.unmodifiableSet(ignoreClanNpcIds) : null; } + public void removeDropGroups() + { + _dropGroups = null; + } + public void removeDrops() { _dropListDeath = null; _dropListSpoil = null; } + public void setDropGroups(List groups) + { + _dropGroups = groups; + } + public void addDrop(DropHolder dropHolder) { if (_dropListDeath == null) @@ -612,6 +624,11 @@ public class NpcTemplate extends CreatureTemplate implements IIdentifiable _dropListSpoil.add(dropHolder); } + public List getDropGroups() + { + return _dropGroups; + } + public List getDropList() { return _dropListDeath; @@ -624,11 +641,201 @@ public class NpcTemplate extends CreatureTemplate implements IIdentifiable public List calculateDrops(DropType dropType, Creature victim, Creature killer) { - final List dropList = dropType == DropType.SPOIL ? _dropListSpoil : _dropListDeath; - if (dropList == null) + if (dropType == DropType.DROP) { - return null; + // calculate group drops + List groupDrops = null; + if (_dropGroups != null) + { + groupDrops = calculateGroupDrops(victim, killer); + } + + // calculate ungrouped drops + List ungroupedDrops = null; + if (_dropListDeath != null) + { + ungroupedDrops = calculateUngroupedDrops(dropType, victim, killer); + } + + // return results + if ((groupDrops != null) && (ungroupedDrops != null)) + { + groupDrops.addAll(ungroupedDrops); + ungroupedDrops.clear(); + return groupDrops; + } + if (groupDrops != null) + { + return groupDrops; + } + if (ungroupedDrops != null) + { + return ungroupedDrops; + } } + else if ((dropType == DropType.SPOIL) && (_dropListSpoil != null)) + { + return calculateUngroupedDrops(dropType, victim, killer); + } + + // no drops + return null; + } + + private List calculateGroupDrops(Creature victim, Creature killer) + { + // level difference calculations + final int levelDifference = victim.getLevel() - killer.getLevel(); + final double levelGapChanceToDropAdena = Util.map(levelDifference, -Config.DROP_ADENA_MAX_LEVEL_DIFFERENCE, -Config.DROP_ADENA_MIN_LEVEL_DIFFERENCE, Config.DROP_ADENA_MIN_LEVEL_GAP_CHANCE, 100d); + final double levelGapChanceToDrop = Util.map(levelDifference, -Config.DROP_ITEM_MAX_LEVEL_DIFFERENCE, -Config.DROP_ITEM_MIN_LEVEL_DIFFERENCE, Config.DROP_ITEM_MIN_LEVEL_GAP_CHANCE, 100d); + + int dropOccurrenceCounter = victim.isRaid() ? Config.DROP_MAX_OCCURRENCES_RAIDBOSS : Config.DROP_MAX_OCCURRENCES_NORMAL; + List calculatedDrops = null; + for (DropGroupHolder group : _dropGroups) + { + if (dropOccurrenceCounter <= 0) + { + break; + } + + double groupRate = 1; + final List groupDrops = group.getDropList(); + for (DropHolder dropItem : groupDrops) + { + final int itemId = dropItem.getItemId(); + final ItemTemplate item = ItemTable.getInstance().getTemplate(itemId); + final boolean champion = victim.isChampion(); + + // chance + double rateChance = 1; + final Float itemChance = Config.RATE_DROP_CHANCE_BY_ID.get(itemId); + if (itemChance != null) + { + if (itemChance == 0) + { + continue; + } + + rateChance *= itemChance; + if (champion && (itemId == Inventory.ADENA_ID)) + { + rateChance *= Config.CHAMPION_ADENAS_REWARDS_CHANCE; + } + } + else if (item.hasExImmediateEffect()) + { + rateChance *= Config.RATE_HERB_DROP_CHANCE_MULTIPLIER; + } + else if (victim.isRaid()) + { + rateChance *= Config.RATE_RAID_DROP_CHANCE_MULTIPLIER; + } + else + { + rateChance *= Config.RATE_DEATH_DROP_CHANCE_MULTIPLIER * (champion ? Config.CHAMPION_REWARDS_CHANCE : 1); + } + + // premium chance + if (Config.PREMIUM_SYSTEM_ENABLED && (killer.getActingPlayer() != null) && killer.getActingPlayer().hasPremiumStatus()) + { + if (Config.PREMIUM_RATE_DROP_CHANCE_BY_ID.get(itemId) != null) + { + rateChance *= Config.PREMIUM_RATE_DROP_CHANCE_BY_ID.get(itemId); + } + else if (item.hasExImmediateEffect()) + { + // TODO: Premium herb chance? :) + } + else if (victim.isRaid()) + { + // TODO: Premium raid chance? :) + } + else + { + rateChance *= Config.PREMIUM_RATE_DROP_CHANCE; + } + } + + // keep lowest to avoid chance by id configuration conflicts + groupRate = Math.min(groupRate, rateChance); + } + + if ((Rnd.nextDouble() * 100) < (group.getChance() * groupRate)) + { + double totalChance = 0; // total group chance is 100 + final double dropChance = Rnd.nextDouble() * 100; + GROUP_DROP: for (DropHolder dropItem : groupDrops) + { + if (dropOccurrenceCounter <= 0) + { + break; + } + + // calculate if item will drop + totalChance += dropItem.getChance(); + if (dropChance >= totalChance) + { + continue; + } + + // check level gap that may prevent to drop item + if ((Rnd.nextDouble() * 100) > (dropItem.getItemId() == Inventory.ADENA_ID ? levelGapChanceToDropAdena : levelGapChanceToDrop)) + { + continue; + } + + // create the drop + final ItemHolder drop = calculateGroupDrop(dropItem, victim, killer); + + // create list + if (calculatedDrops == null) + { + calculatedDrops = new ArrayList<>(dropOccurrenceCounter); + } + + // finally + if (group.getChance() < 100) + { + dropOccurrenceCounter--; + } + calculatedDrops.add(drop); + + // no more drops from this group + break GROUP_DROP; + } + } + } + + // champion extra drop + if (victim.isChampion()) + { + if ((victim.getLevel() < killer.getLevel()) && (Rnd.get(100) < Config.CHAMPION_REWARD_LOWER_LEVEL_ITEM_CHANCE)) + { + return calculatedDrops; + } + if ((victim.getLevel() > killer.getLevel()) && (Rnd.get(100) < Config.CHAMPION_REWARD_HIGHER_LEVEL_ITEM_CHANCE)) + { + return calculatedDrops; + } + + // create list + if (calculatedDrops == null) + { + calculatedDrops = new ArrayList<>(); + } + + if (!calculatedDrops.containsAll(Config.CHAMPION_REWARD_ITEMS)) + { + calculatedDrops.addAll(Config.CHAMPION_REWARD_ITEMS); + } + } + + return calculatedDrops; + } + + private List calculateUngroupedDrops(DropType dropType, Creature victim, Creature killer) + { + final List dropList = dropType == DropType.SPOIL ? _dropListSpoil : _dropListDeath; // level difference calculations final int levelDifference = victim.getLevel() - killer.getLevel(); @@ -660,7 +867,7 @@ public class NpcTemplate extends CreatureTemplate implements IIdentifiable } // calculate chances - final ItemHolder drop = calculateDrop(dropItem, victim, killer); + final ItemHolder drop = calculateUngroupedDrop(dropItem, victim, killer); if (drop == null) { continue; @@ -715,20 +922,82 @@ public class NpcTemplate extends CreatureTemplate implements IIdentifiable calculatedDrops = new ArrayList<>(); } - calculatedDrops.addAll(Config.CHAMPION_REWARD_ITEMS); + if (!calculatedDrops.containsAll(Config.CHAMPION_REWARD_ITEMS)) + { + calculatedDrops.addAll(Config.CHAMPION_REWARD_ITEMS); + } } return calculatedDrops; } /** - * All item drop chance calculations are done by this method. * @param dropItem * @param victim * @param killer * @return ItemHolder */ - private ItemHolder calculateDrop(DropHolder dropItem, Creature victim, Creature killer) + private ItemHolder calculateGroupDrop(DropHolder dropItem, Creature victim, Creature killer) + { + final int itemId = dropItem.getItemId(); + final ItemTemplate item = ItemTable.getInstance().getTemplate(itemId); + final boolean champion = victim.isChampion(); + + // calculate amount + double rateAmount = 1; + if (Config.RATE_DROP_AMOUNT_BY_ID.get(itemId) != null) + { + rateAmount *= Config.RATE_DROP_AMOUNT_BY_ID.get(itemId); + if (champion && (itemId == Inventory.ADENA_ID)) + { + rateAmount *= Config.CHAMPION_ADENAS_REWARDS_AMOUNT; + } + } + else if (item.hasExImmediateEffect()) + { + rateAmount *= Config.RATE_HERB_DROP_AMOUNT_MULTIPLIER; + } + else if (victim.isRaid()) + { + rateAmount *= Config.RATE_RAID_DROP_AMOUNT_MULTIPLIER; + } + else + { + rateAmount *= Config.RATE_DEATH_DROP_AMOUNT_MULTIPLIER * (champion ? Config.CHAMPION_REWARDS_AMOUNT : 1); + } + + // premium amount + if (Config.PREMIUM_SYSTEM_ENABLED && (killer.getActingPlayer() != null) && killer.getActingPlayer().hasPremiumStatus()) + { + if (Config.PREMIUM_RATE_DROP_AMOUNT_BY_ID.get(itemId) != null) + { + rateAmount *= Config.PREMIUM_RATE_DROP_AMOUNT_BY_ID.get(itemId); + } + else if (item.hasExImmediateEffect()) + { + // TODO: Premium herb amount? :) + } + else if (victim.isRaid()) + { + // TODO: Premium raid amount? :) + } + else + { + rateAmount *= Config.PREMIUM_RATE_DROP_AMOUNT; + } + } + + // finally + return new ItemHolder(itemId, (long) (Rnd.get(dropItem.getMin(), dropItem.getMax()) * rateAmount)); + } + + /** + * @param dropItem + * @param victim + * @param killer + * @return ItemHolder + */ + private ItemHolder calculateUngroupedDrop(DropHolder dropItem, Creature victim, Creature killer) { switch (dropItem.getDropType()) { diff --git a/L2J_Mobius_CT_2.4_Epilogue/java/org/l2jmobius/gameserver/model/holders/DropGroupHolder.java b/L2J_Mobius_CT_2.4_Epilogue/java/org/l2jmobius/gameserver/model/holders/DropGroupHolder.java new file mode 100644 index 0000000000..6c2401c0b0 --- /dev/null +++ b/L2J_Mobius_CT_2.4_Epilogue/java/org/l2jmobius/gameserver/model/holders/DropGroupHolder.java @@ -0,0 +1,49 @@ +/* + * This file is part of the L2J Mobius project. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package org.l2jmobius.gameserver.model.holders; + +import java.util.ArrayList; +import java.util.List; + +/** + * @author Mobius + */ +public class DropGroupHolder +{ + private final List _dropList = new ArrayList<>(); + private final double _chance; + + public DropGroupHolder(double chance) + { + _chance = chance; + } + + public List getDropList() + { + return _dropList; + } + + public void addDrop(DropHolder holder) + { + _dropList.add(holder); + } + + public double getChance() + { + return _chance; + } +} diff --git a/L2J_Mobius_CT_2.6_HighFive/dist/game/data/scripts/handlers/bypasshandlers/NpcViewMod.java b/L2J_Mobius_CT_2.6_HighFive/dist/game/data/scripts/handlers/bypasshandlers/NpcViewMod.java index c8233d9ee1..854d5d858a 100644 --- a/L2J_Mobius_CT_2.6_HighFive/dist/game/data/scripts/handlers/bypasshandlers/NpcViewMod.java +++ b/L2J_Mobius_CT_2.6_HighFive/dist/game/data/scripts/handlers/bypasshandlers/NpcViewMod.java @@ -35,6 +35,7 @@ import org.l2jmobius.gameserver.model.WorldObject; import org.l2jmobius.gameserver.model.actor.Creature; import org.l2jmobius.gameserver.model.actor.Npc; import org.l2jmobius.gameserver.model.actor.Player; +import org.l2jmobius.gameserver.model.holders.DropGroupHolder; import org.l2jmobius.gameserver.model.holders.DropHolder; import org.l2jmobius.gameserver.model.item.ItemTemplate; import org.l2jmobius.gameserver.network.serverpackets.NpcHtmlMessage; @@ -206,12 +207,13 @@ public class NpcViewMod implements IBypassHandler private static String getDropListButtons(Npc npc) { final StringBuilder sb = new StringBuilder(); + final List dropListGroups = npc.getTemplate().getDropGroups(); final List dropListDeath = npc.getTemplate().getDropList(); final List dropListSpoil = npc.getTemplate().getSpoilList(); - if ((dropListDeath != null) || (dropListSpoil != null)) + if ((dropListGroups != null) || (dropListDeath != null) || (dropListSpoil != null)) { sb.append("
"); - if (dropListDeath != null) + if ((dropListGroups != null) || (dropListDeath != null)) { sb.append(""); } @@ -228,13 +230,40 @@ public class NpcViewMod implements IBypassHandler private void sendNpcDropList(Player player, Npc npc, DropType dropType, int pageValue) { - final List templateList = dropType == DropType.SPOIL ? npc.getTemplate().getSpoilList() : npc.getTemplate().getDropList(); - if (templateList == null) + List dropList = null; + if (dropType == DropType.SPOIL) + { + dropList = new ArrayList<>(npc.getTemplate().getSpoilList()); + } + else + { + final List drops = npc.getTemplate().getDropList(); + if (drops != null) + { + dropList = new ArrayList<>(drops); + } + final List dropGroups = npc.getTemplate().getDropGroups(); + if (dropGroups != null) + { + if (dropList == null) + { + dropList = new ArrayList<>(); + } + for (DropGroupHolder dropGroup : dropGroups) + { + final double chance = dropGroup.getChance() / 100; + for (DropHolder dropHolder : dropGroup.getDropList()) + { + dropList.add(new DropHolder(dropHolder.getDropType(), dropHolder.getItemId(), dropHolder.getMin(), dropHolder.getMax(), dropHolder.getChance() * chance)); + } + } + } + } + if (dropList == null) { return; } - final List dropList = new ArrayList<>(templateList); Collections.sort(dropList, (d1, d2) -> Integer.valueOf(d1.getItemId()).compareTo(Integer.valueOf(d2.getItemId()))); int pages = dropList.size() / DROP_LIST_ITEMS_PER_PAGE; diff --git a/L2J_Mobius_CT_2.6_HighFive/dist/game/data/scripts/handlers/communityboard/DropSearchBoard.java b/L2J_Mobius_CT_2.6_HighFive/dist/game/data/scripts/handlers/communityboard/DropSearchBoard.java index e2cef26f29..51f76f73fa 100644 --- a/L2J_Mobius_CT_2.6_HighFive/dist/game/data/scripts/handlers/communityboard/DropSearchBoard.java +++ b/L2J_Mobius_CT_2.6_HighFive/dist/game/data/scripts/handlers/communityboard/DropSearchBoard.java @@ -36,6 +36,7 @@ import org.l2jmobius.gameserver.handler.IParseBoardHandler; import org.l2jmobius.gameserver.model.Spawn; import org.l2jmobius.gameserver.model.actor.Player; import org.l2jmobius.gameserver.model.actor.templates.NpcTemplate; +import org.l2jmobius.gameserver.model.holders.DropGroupHolder; import org.l2jmobius.gameserver.model.holders.DropHolder; import org.l2jmobius.gameserver.model.item.ItemTemplate; import org.l2jmobius.gameserver.model.itemcontainer.Inventory; @@ -102,6 +103,17 @@ public class DropSearchBoard implements IParseBoardHandler private void buildDropIndex() { + NpcData.getInstance().getTemplates(npc -> npc.getDropGroups() != null).forEach(npcTemplate -> + { + for (DropGroupHolder dropGroup : npcTemplate.getDropGroups()) + { + final double chance = dropGroup.getChance() / 100; + for (DropHolder dropHolder : dropGroup.getDropList()) + { + addToDropList(npcTemplate, new DropHolder(dropHolder.getDropType(), dropHolder.getItemId(), dropHolder.getMin(), dropHolder.getMax(), dropHolder.getChance() * chance)); + } + } + }); NpcData.getInstance().getTemplates(npc -> npc.getDropList() != null).forEach(npcTemplate -> { for (DropHolder dropHolder : npcTemplate.getDropList()) diff --git a/L2J_Mobius_CT_2.6_HighFive/dist/game/data/xsd/npcs.xsd b/L2J_Mobius_CT_2.6_HighFive/dist/game/data/xsd/npcs.xsd index 54228dcc3e..43979e19b5 100644 --- a/L2J_Mobius_CT_2.6_HighFive/dist/game/data/xsd/npcs.xsd +++ b/L2J_Mobius_CT_2.6_HighFive/dist/game/data/xsd/npcs.xsd @@ -13,7 +13,67 @@ - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/L2J_Mobius_CT_2.6_HighFive/java/org/l2jmobius/gameserver/data/xml/NpcData.java b/L2J_Mobius_CT_2.6_HighFive/java/org/l2jmobius/gameserver/data/xml/NpcData.java index dbb14441c6..a0b90059fb 100644 --- a/L2J_Mobius_CT_2.6_HighFive/java/org/l2jmobius/gameserver/data/xml/NpcData.java +++ b/L2J_Mobius_CT_2.6_HighFive/java/org/l2jmobius/gameserver/data/xml/NpcData.java @@ -43,6 +43,7 @@ import org.l2jmobius.gameserver.enums.DropType; import org.l2jmobius.gameserver.model.StatSet; import org.l2jmobius.gameserver.model.actor.templates.NpcTemplate; import org.l2jmobius.gameserver.model.effects.EffectType; +import org.l2jmobius.gameserver.model.holders.DropGroupHolder; import org.l2jmobius.gameserver.model.holders.DropHolder; import org.l2jmobius.gameserver.model.holders.MinionHolder; import org.l2jmobius.gameserver.model.holders.SkillHolder; @@ -99,6 +100,7 @@ public class NpcData implements IXmlReader Set clans = null; Set ignoreClanNpcIds = null; List dropLists = null; + List dropGroups = null; set.set("id", npcId); set.set("displayId", parseInteger(attrs, "displayId")); set.set("level", parseByte(attrs, "level")); @@ -456,24 +458,54 @@ public class NpcData implements IXmlReader if (dropType != null) { - if (dropLists == null) - { - dropLists = new ArrayList<>(); - } - for (Node dropNode = dropListsNode.getFirstChild(); dropNode != null; dropNode = dropNode.getNextSibling()) { - final NamedNodeMap dropAttrs = dropNode.getAttributes(); - if ("item".equalsIgnoreCase(dropNode.getNodeName())) + final String nodeName = dropNode.getNodeName(); + if (nodeName.equalsIgnoreCase("group")) { - final DropHolder dropItem = new DropHolder(dropType, parseInteger(dropAttrs, "id"), parseLong(dropAttrs, "min"), parseLong(dropAttrs, "max"), parseDouble(dropAttrs, "chance")); - if (ItemTable.getInstance().getTemplate(parseInteger(dropAttrs, "id")) == null) + if (dropGroups == null) { - LOGGER.warning("DropListItem: Could not find item with id " + parseInteger(dropAttrs, "id") + "."); + dropGroups = new ArrayList<>(); + } + + final DropGroupHolder group = new DropGroupHolder(parseDouble(dropNode.getAttributes(), "chance")); + for (Node groupNode = dropNode.getFirstChild(); groupNode != null; groupNode = groupNode.getNextSibling()) + { + if (groupNode.getNodeName().equalsIgnoreCase("item")) + { + final NamedNodeMap groupAttrs = groupNode.getAttributes(); + final int itemId = parseInteger(groupAttrs, "id"); + + if (ItemTable.getInstance().getTemplate(itemId) == null) + { + LOGGER.warning("DropListItem: Could not find item with id " + itemId + "."); + } + else + { + group.addDrop(new DropHolder(dropType, itemId, parseLong(groupAttrs, "min"), parseLong(groupAttrs, "max"), parseDouble(groupAttrs, "chance"))); + } + } + } + + dropGroups.add(group); + } + else if (nodeName.equalsIgnoreCase("item")) + { + if (dropLists == null) + { + dropLists = new ArrayList<>(); + } + + final NamedNodeMap dropAttrs = dropNode.getAttributes(); + final int itemId = parseInteger(dropAttrs, "id"); + + if (ItemTable.getInstance().getTemplate(itemId) == null) + { + LOGGER.warning("DropListItem: Could not find item with id " + itemId + "."); } else { - dropLists.add(dropItem); + dropLists.add(new DropHolder(dropType, itemId, parseLong(dropAttrs, "min"), parseLong(dropAttrs, "max"), parseDouble(dropAttrs, "chance"))); } } } @@ -630,10 +662,21 @@ public class NpcData implements IXmlReader template.setClans(clans); template.setIgnoreClanNpcIds(ignoreClanNpcIds); + // Clean old drop groups. + template.removeDropGroups(); + + // Set new drop groups. + if (dropGroups != null) + { + template.setDropGroups(dropGroups); + } + + // Clean old drop lists. + template.removeDrops(); + + // Set new drop lists. if (dropLists != null) { - template.removeDrops(); - // Drops are sorted by chance (high to low). Collections.sort(dropLists, (d1, d2) -> Double.valueOf(d2.getChance()).compareTo(Double.valueOf(d1.getChance()))); for (DropHolder dropHolder : dropLists) diff --git a/L2J_Mobius_CT_2.6_HighFive/java/org/l2jmobius/gameserver/model/actor/templates/NpcTemplate.java b/L2J_Mobius_CT_2.6_HighFive/java/org/l2jmobius/gameserver/model/actor/templates/NpcTemplate.java index 3d4eded097..83fadfc4d7 100644 --- a/L2J_Mobius_CT_2.6_HighFive/java/org/l2jmobius/gameserver/model/actor/templates/NpcTemplate.java +++ b/L2J_Mobius_CT_2.6_HighFive/java/org/l2jmobius/gameserver/model/actor/templates/NpcTemplate.java @@ -34,6 +34,7 @@ import org.l2jmobius.gameserver.enums.Race; import org.l2jmobius.gameserver.enums.Sex; import org.l2jmobius.gameserver.model.StatSet; import org.l2jmobius.gameserver.model.actor.Creature; +import org.l2jmobius.gameserver.model.holders.DropGroupHolder; import org.l2jmobius.gameserver.model.holders.DropHolder; import org.l2jmobius.gameserver.model.holders.ItemHolder; import org.l2jmobius.gameserver.model.interfaces.IIdentifiable; @@ -98,6 +99,7 @@ public class NpcTemplate extends CreatureTemplate implements IIdentifiable private Map> _aiSkillLists; private Set _clans; private Set _ignoreClanNpcIds; + private List _dropGroups; private List _dropListDeath; private List _dropListSpoil; private double _collisionRadiusGrown; @@ -588,12 +590,22 @@ public class NpcTemplate extends CreatureTemplate implements IIdentifiable _ignoreClanNpcIds = ignoreClanNpcIds != null ? Collections.unmodifiableSet(ignoreClanNpcIds) : null; } + public void removeDropGroups() + { + _dropGroups = null; + } + public void removeDrops() { _dropListDeath = null; _dropListSpoil = null; } + public void setDropGroups(List groups) + { + _dropGroups = groups; + } + public void addDrop(DropHolder dropHolder) { if (_dropListDeath == null) @@ -612,6 +624,11 @@ public class NpcTemplate extends CreatureTemplate implements IIdentifiable _dropListSpoil.add(dropHolder); } + public List getDropGroups() + { + return _dropGroups; + } + public List getDropList() { return _dropListDeath; @@ -624,11 +641,201 @@ public class NpcTemplate extends CreatureTemplate implements IIdentifiable public List calculateDrops(DropType dropType, Creature victim, Creature killer) { - final List dropList = dropType == DropType.SPOIL ? _dropListSpoil : _dropListDeath; - if (dropList == null) + if (dropType == DropType.DROP) { - return null; + // calculate group drops + List groupDrops = null; + if (_dropGroups != null) + { + groupDrops = calculateGroupDrops(victim, killer); + } + + // calculate ungrouped drops + List ungroupedDrops = null; + if (_dropListDeath != null) + { + ungroupedDrops = calculateUngroupedDrops(dropType, victim, killer); + } + + // return results + if ((groupDrops != null) && (ungroupedDrops != null)) + { + groupDrops.addAll(ungroupedDrops); + ungroupedDrops.clear(); + return groupDrops; + } + if (groupDrops != null) + { + return groupDrops; + } + if (ungroupedDrops != null) + { + return ungroupedDrops; + } } + else if ((dropType == DropType.SPOIL) && (_dropListSpoil != null)) + { + return calculateUngroupedDrops(dropType, victim, killer); + } + + // no drops + return null; + } + + private List calculateGroupDrops(Creature victim, Creature killer) + { + // level difference calculations + final int levelDifference = victim.getLevel() - killer.getLevel(); + final double levelGapChanceToDropAdena = Util.map(levelDifference, -Config.DROP_ADENA_MAX_LEVEL_DIFFERENCE, -Config.DROP_ADENA_MIN_LEVEL_DIFFERENCE, Config.DROP_ADENA_MIN_LEVEL_GAP_CHANCE, 100d); + final double levelGapChanceToDrop = Util.map(levelDifference, -Config.DROP_ITEM_MAX_LEVEL_DIFFERENCE, -Config.DROP_ITEM_MIN_LEVEL_DIFFERENCE, Config.DROP_ITEM_MIN_LEVEL_GAP_CHANCE, 100d); + + int dropOccurrenceCounter = victim.isRaid() ? Config.DROP_MAX_OCCURRENCES_RAIDBOSS : Config.DROP_MAX_OCCURRENCES_NORMAL; + List calculatedDrops = null; + for (DropGroupHolder group : _dropGroups) + { + if (dropOccurrenceCounter <= 0) + { + break; + } + + double groupRate = 1; + final List groupDrops = group.getDropList(); + for (DropHolder dropItem : groupDrops) + { + final int itemId = dropItem.getItemId(); + final ItemTemplate item = ItemTable.getInstance().getTemplate(itemId); + final boolean champion = victim.isChampion(); + + // chance + double rateChance = 1; + final Float itemChance = Config.RATE_DROP_CHANCE_BY_ID.get(itemId); + if (itemChance != null) + { + if (itemChance == 0) + { + continue; + } + + rateChance *= itemChance; + if (champion && (itemId == Inventory.ADENA_ID)) + { + rateChance *= Config.CHAMPION_ADENAS_REWARDS_CHANCE; + } + } + else if (item.hasExImmediateEffect()) + { + rateChance *= Config.RATE_HERB_DROP_CHANCE_MULTIPLIER; + } + else if (victim.isRaid()) + { + rateChance *= Config.RATE_RAID_DROP_CHANCE_MULTIPLIER; + } + else + { + rateChance *= Config.RATE_DEATH_DROP_CHANCE_MULTIPLIER * (champion ? Config.CHAMPION_REWARDS_CHANCE : 1); + } + + // premium chance + if (Config.PREMIUM_SYSTEM_ENABLED && (killer.getActingPlayer() != null) && killer.getActingPlayer().hasPremiumStatus()) + { + if (Config.PREMIUM_RATE_DROP_CHANCE_BY_ID.get(itemId) != null) + { + rateChance *= Config.PREMIUM_RATE_DROP_CHANCE_BY_ID.get(itemId); + } + else if (item.hasExImmediateEffect()) + { + // TODO: Premium herb chance? :) + } + else if (victim.isRaid()) + { + // TODO: Premium raid chance? :) + } + else + { + rateChance *= Config.PREMIUM_RATE_DROP_CHANCE; + } + } + + // keep lowest to avoid chance by id configuration conflicts + groupRate = Math.min(groupRate, rateChance); + } + + if ((Rnd.nextDouble() * 100) < (group.getChance() * groupRate)) + { + double totalChance = 0; // total group chance is 100 + final double dropChance = Rnd.nextDouble() * 100; + GROUP_DROP: for (DropHolder dropItem : groupDrops) + { + if (dropOccurrenceCounter <= 0) + { + break; + } + + // calculate if item will drop + totalChance += dropItem.getChance(); + if (dropChance >= totalChance) + { + continue; + } + + // check level gap that may prevent to drop item + if ((Rnd.nextDouble() * 100) > (dropItem.getItemId() == Inventory.ADENA_ID ? levelGapChanceToDropAdena : levelGapChanceToDrop)) + { + continue; + } + + // create the drop + final ItemHolder drop = calculateGroupDrop(dropItem, victim, killer); + + // create list + if (calculatedDrops == null) + { + calculatedDrops = new ArrayList<>(dropOccurrenceCounter); + } + + // finally + if (group.getChance() < 100) + { + dropOccurrenceCounter--; + } + calculatedDrops.add(drop); + + // no more drops from this group + break GROUP_DROP; + } + } + } + + // champion extra drop + if (victim.isChampion()) + { + if ((victim.getLevel() < killer.getLevel()) && (Rnd.get(100) < Config.CHAMPION_REWARD_LOWER_LEVEL_ITEM_CHANCE)) + { + return calculatedDrops; + } + if ((victim.getLevel() > killer.getLevel()) && (Rnd.get(100) < Config.CHAMPION_REWARD_HIGHER_LEVEL_ITEM_CHANCE)) + { + return calculatedDrops; + } + + // create list + if (calculatedDrops == null) + { + calculatedDrops = new ArrayList<>(); + } + + if (!calculatedDrops.containsAll(Config.CHAMPION_REWARD_ITEMS)) + { + calculatedDrops.addAll(Config.CHAMPION_REWARD_ITEMS); + } + } + + return calculatedDrops; + } + + private List calculateUngroupedDrops(DropType dropType, Creature victim, Creature killer) + { + final List dropList = dropType == DropType.SPOIL ? _dropListSpoil : _dropListDeath; // level difference calculations final int levelDifference = victim.getLevel() - killer.getLevel(); @@ -660,7 +867,7 @@ public class NpcTemplate extends CreatureTemplate implements IIdentifiable } // calculate chances - final ItemHolder drop = calculateDrop(dropItem, victim, killer); + final ItemHolder drop = calculateUngroupedDrop(dropItem, victim, killer); if (drop == null) { continue; @@ -715,20 +922,82 @@ public class NpcTemplate extends CreatureTemplate implements IIdentifiable calculatedDrops = new ArrayList<>(); } - calculatedDrops.addAll(Config.CHAMPION_REWARD_ITEMS); + if (!calculatedDrops.containsAll(Config.CHAMPION_REWARD_ITEMS)) + { + calculatedDrops.addAll(Config.CHAMPION_REWARD_ITEMS); + } } return calculatedDrops; } /** - * All item drop chance calculations are done by this method. * @param dropItem * @param victim * @param killer * @return ItemHolder */ - private ItemHolder calculateDrop(DropHolder dropItem, Creature victim, Creature killer) + private ItemHolder calculateGroupDrop(DropHolder dropItem, Creature victim, Creature killer) + { + final int itemId = dropItem.getItemId(); + final ItemTemplate item = ItemTable.getInstance().getTemplate(itemId); + final boolean champion = victim.isChampion(); + + // calculate amount + double rateAmount = 1; + if (Config.RATE_DROP_AMOUNT_BY_ID.get(itemId) != null) + { + rateAmount *= Config.RATE_DROP_AMOUNT_BY_ID.get(itemId); + if (champion && (itemId == Inventory.ADENA_ID)) + { + rateAmount *= Config.CHAMPION_ADENAS_REWARDS_AMOUNT; + } + } + else if (item.hasExImmediateEffect()) + { + rateAmount *= Config.RATE_HERB_DROP_AMOUNT_MULTIPLIER; + } + else if (victim.isRaid()) + { + rateAmount *= Config.RATE_RAID_DROP_AMOUNT_MULTIPLIER; + } + else + { + rateAmount *= Config.RATE_DEATH_DROP_AMOUNT_MULTIPLIER * (champion ? Config.CHAMPION_REWARDS_AMOUNT : 1); + } + + // premium amount + if (Config.PREMIUM_SYSTEM_ENABLED && (killer.getActingPlayer() != null) && killer.getActingPlayer().hasPremiumStatus()) + { + if (Config.PREMIUM_RATE_DROP_AMOUNT_BY_ID.get(itemId) != null) + { + rateAmount *= Config.PREMIUM_RATE_DROP_AMOUNT_BY_ID.get(itemId); + } + else if (item.hasExImmediateEffect()) + { + // TODO: Premium herb amount? :) + } + else if (victim.isRaid()) + { + // TODO: Premium raid amount? :) + } + else + { + rateAmount *= Config.PREMIUM_RATE_DROP_AMOUNT; + } + } + + // finally + return new ItemHolder(itemId, (long) (Rnd.get(dropItem.getMin(), dropItem.getMax()) * rateAmount)); + } + + /** + * @param dropItem + * @param victim + * @param killer + * @return ItemHolder + */ + private ItemHolder calculateUngroupedDrop(DropHolder dropItem, Creature victim, Creature killer) { switch (dropItem.getDropType()) { diff --git a/L2J_Mobius_CT_2.6_HighFive/java/org/l2jmobius/gameserver/model/holders/DropGroupHolder.java b/L2J_Mobius_CT_2.6_HighFive/java/org/l2jmobius/gameserver/model/holders/DropGroupHolder.java new file mode 100644 index 0000000000..6c2401c0b0 --- /dev/null +++ b/L2J_Mobius_CT_2.6_HighFive/java/org/l2jmobius/gameserver/model/holders/DropGroupHolder.java @@ -0,0 +1,49 @@ +/* + * This file is part of the L2J Mobius project. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package org.l2jmobius.gameserver.model.holders; + +import java.util.ArrayList; +import java.util.List; + +/** + * @author Mobius + */ +public class DropGroupHolder +{ + private final List _dropList = new ArrayList<>(); + private final double _chance; + + public DropGroupHolder(double chance) + { + _chance = chance; + } + + public List getDropList() + { + return _dropList; + } + + public void addDrop(DropHolder holder) + { + _dropList.add(holder); + } + + public double getChance() + { + return _chance; + } +} diff --git a/L2J_Mobius_Classic_2.0_Saviors/dist/game/data/scripts/handlers/bypasshandlers/NpcViewMod.java b/L2J_Mobius_Classic_2.0_Saviors/dist/game/data/scripts/handlers/bypasshandlers/NpcViewMod.java index 2e5b2846bf..38ca23ffc7 100644 --- a/L2J_Mobius_Classic_2.0_Saviors/dist/game/data/scripts/handlers/bypasshandlers/NpcViewMod.java +++ b/L2J_Mobius_Classic_2.0_Saviors/dist/game/data/scripts/handlers/bypasshandlers/NpcViewMod.java @@ -37,6 +37,7 @@ import org.l2jmobius.gameserver.model.actor.Attackable; import org.l2jmobius.gameserver.model.actor.Creature; import org.l2jmobius.gameserver.model.actor.Npc; import org.l2jmobius.gameserver.model.actor.Player; +import org.l2jmobius.gameserver.model.holders.DropGroupHolder; import org.l2jmobius.gameserver.model.holders.DropHolder; import org.l2jmobius.gameserver.model.item.ItemTemplate; import org.l2jmobius.gameserver.model.itemcontainer.Inventory; @@ -326,12 +327,13 @@ public class NpcViewMod implements IBypassHandler private static String getDropListButtons(Npc npc) { final StringBuilder sb = new StringBuilder(); + final List dropListGroups = npc.getTemplate().getDropGroups(); final List dropListDeath = npc.getTemplate().getDropList(); final List dropListSpoil = npc.getTemplate().getSpoilList(); - if ((dropListDeath != null) || (dropListSpoil != null)) + if ((dropListGroups != null) || (dropListDeath != null) || (dropListSpoil != null)) { sb.append("
"); - if (dropListDeath != null) + if ((dropListGroups != null) || (dropListDeath != null)) { sb.append(""); } @@ -348,13 +350,40 @@ public class NpcViewMod implements IBypassHandler private void sendNpcDropList(Player player, Npc npc, DropType dropType, int pageValue) { - final List templateList = dropType == DropType.SPOIL ? npc.getTemplate().getSpoilList() : npc.getTemplate().getDropList(); - if (templateList == null) + List dropList = null; + if (dropType == DropType.SPOIL) + { + dropList = new ArrayList<>(npc.getTemplate().getSpoilList()); + } + else + { + final List drops = npc.getTemplate().getDropList(); + if (drops != null) + { + dropList = new ArrayList<>(drops); + } + final List dropGroups = npc.getTemplate().getDropGroups(); + if (dropGroups != null) + { + if (dropList == null) + { + dropList = new ArrayList<>(); + } + for (DropGroupHolder dropGroup : dropGroups) + { + final double chance = dropGroup.getChance() / 100; + for (DropHolder dropHolder : dropGroup.getDropList()) + { + dropList.add(new DropHolder(dropHolder.getDropType(), dropHolder.getItemId(), dropHolder.getMin(), dropHolder.getMax(), dropHolder.getChance() * chance)); + } + } + } + } + if (dropList == null) { return; } - final List dropList = new ArrayList<>(templateList); Collections.sort(dropList, (d1, d2) -> Integer.valueOf(d1.getItemId()).compareTo(Integer.valueOf(d2.getItemId()))); int pages = dropList.size() / DROP_LIST_ITEMS_PER_PAGE; diff --git a/L2J_Mobius_Classic_2.0_Saviors/dist/game/data/scripts/handlers/communityboard/DropSearchBoard.java b/L2J_Mobius_Classic_2.0_Saviors/dist/game/data/scripts/handlers/communityboard/DropSearchBoard.java index 647d14cec5..be1d97660b 100644 --- a/L2J_Mobius_Classic_2.0_Saviors/dist/game/data/scripts/handlers/communityboard/DropSearchBoard.java +++ b/L2J_Mobius_Classic_2.0_Saviors/dist/game/data/scripts/handlers/communityboard/DropSearchBoard.java @@ -36,6 +36,7 @@ import org.l2jmobius.gameserver.handler.CommunityBoardHandler; import org.l2jmobius.gameserver.handler.IParseBoardHandler; import org.l2jmobius.gameserver.model.actor.Player; import org.l2jmobius.gameserver.model.actor.templates.NpcTemplate; +import org.l2jmobius.gameserver.model.holders.DropGroupHolder; import org.l2jmobius.gameserver.model.holders.DropHolder; import org.l2jmobius.gameserver.model.item.ItemTemplate; import org.l2jmobius.gameserver.model.itemcontainer.Inventory; @@ -104,6 +105,17 @@ public class DropSearchBoard implements IParseBoardHandler private void buildDropIndex() { + NpcData.getInstance().getTemplates(npc -> npc.getDropGroups() != null).forEach(npcTemplate -> + { + for (DropGroupHolder dropGroup : npcTemplate.getDropGroups()) + { + final double chance = dropGroup.getChance() / 100; + for (DropHolder dropHolder : dropGroup.getDropList()) + { + addToDropList(npcTemplate, new DropHolder(dropHolder.getDropType(), dropHolder.getItemId(), dropHolder.getMin(), dropHolder.getMax(), dropHolder.getChance() * chance)); + } + } + }); NpcData.getInstance().getTemplates(npc -> npc.getDropList() != null).forEach(npcTemplate -> { for (DropHolder dropHolder : npcTemplate.getDropList()) diff --git a/L2J_Mobius_Classic_2.0_Saviors/dist/game/data/xsd/npcs.xsd b/L2J_Mobius_Classic_2.0_Saviors/dist/game/data/xsd/npcs.xsd index 117ca4cc1d..2c6e583383 100644 --- a/L2J_Mobius_Classic_2.0_Saviors/dist/game/data/xsd/npcs.xsd +++ b/L2J_Mobius_Classic_2.0_Saviors/dist/game/data/xsd/npcs.xsd @@ -1,16 +1,5 @@ - - - - - - - - - - - @@ -260,11 +249,81 @@ - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/L2J_Mobius_Classic_2.0_Saviors/java/org/l2jmobius/gameserver/data/xml/NpcData.java b/L2J_Mobius_Classic_2.0_Saviors/java/org/l2jmobius/gameserver/data/xml/NpcData.java index 28850ef6bb..d6d5210168 100644 --- a/L2J_Mobius_Classic_2.0_Saviors/java/org/l2jmobius/gameserver/data/xml/NpcData.java +++ b/L2J_Mobius_Classic_2.0_Saviors/java/org/l2jmobius/gameserver/data/xml/NpcData.java @@ -46,6 +46,7 @@ import org.l2jmobius.gameserver.enums.MpRewardType; import org.l2jmobius.gameserver.model.StatSet; import org.l2jmobius.gameserver.model.actor.templates.NpcTemplate; import org.l2jmobius.gameserver.model.effects.EffectType; +import org.l2jmobius.gameserver.model.holders.DropGroupHolder; import org.l2jmobius.gameserver.model.holders.DropHolder; import org.l2jmobius.gameserver.model.skill.Skill; @@ -100,6 +101,7 @@ public class NpcData implements IXmlReader Set clans = null; Set ignoreClanNpcIds = null; List dropLists = null; + List dropGroups = null; set.set("id", npcId); set.set("displayId", parseInteger(attrs, "displayId")); set.set("level", parseByte(attrs, "level")); @@ -436,24 +438,54 @@ public class NpcData implements IXmlReader if (dropType != null) { - if (dropLists == null) - { - dropLists = new ArrayList<>(); - } - for (Node dropNode = dropListsNode.getFirstChild(); dropNode != null; dropNode = dropNode.getNextSibling()) { - final NamedNodeMap dropAttrs = dropNode.getAttributes(); - if ("item".equalsIgnoreCase(dropNode.getNodeName())) + final String nodeName = dropNode.getNodeName(); + if (nodeName.equalsIgnoreCase("group")) { - final DropHolder dropItem = new DropHolder(dropType, parseInteger(dropAttrs, "id"), parseLong(dropAttrs, "min"), parseLong(dropAttrs, "max"), parseDouble(dropAttrs, "chance")); - if (ItemTable.getInstance().getTemplate(parseInteger(dropAttrs, "id")) == null) + if (dropGroups == null) { - LOGGER.warning("DropListItem: Could not find item with id " + parseInteger(dropAttrs, "id") + "."); + dropGroups = new ArrayList<>(); + } + + final DropGroupHolder group = new DropGroupHolder(parseDouble(dropNode.getAttributes(), "chance")); + for (Node groupNode = dropNode.getFirstChild(); groupNode != null; groupNode = groupNode.getNextSibling()) + { + if (groupNode.getNodeName().equalsIgnoreCase("item")) + { + final NamedNodeMap groupAttrs = groupNode.getAttributes(); + final int itemId = parseInteger(groupAttrs, "id"); + + if (ItemTable.getInstance().getTemplate(itemId) == null) + { + LOGGER.warning("DropListItem: Could not find item with id " + itemId + "."); + } + else + { + group.addDrop(new DropHolder(dropType, itemId, parseLong(groupAttrs, "min"), parseLong(groupAttrs, "max"), parseDouble(groupAttrs, "chance"))); + } + } + } + + dropGroups.add(group); + } + else if (nodeName.equalsIgnoreCase("item")) + { + if (dropLists == null) + { + dropLists = new ArrayList<>(); + } + + final NamedNodeMap dropAttrs = dropNode.getAttributes(); + final int itemId = parseInteger(dropAttrs, "id"); + + if (ItemTable.getInstance().getTemplate(itemId) == null) + { + LOGGER.warning("DropListItem: Could not find item with id " + itemId + "."); } else { - dropLists.add(dropItem); + dropLists.add(new DropHolder(dropType, itemId, parseLong(dropAttrs, "min"), parseLong(dropAttrs, "max"), parseDouble(dropAttrs, "chance"))); } } } @@ -612,10 +644,21 @@ public class NpcData implements IXmlReader template.setClans(clans); template.setIgnoreClanNpcIds(ignoreClanNpcIds); + // Clean old drop groups. + template.removeDropGroups(); + + // Set new drop groups. + if (dropGroups != null) + { + template.setDropGroups(dropGroups); + } + + // Clean old drop lists. + template.removeDrops(); + + // Set new drop lists. if (dropLists != null) { - template.removeDrops(); - // Drops are sorted by chance (high to low). Collections.sort(dropLists, (d1, d2) -> Double.valueOf(d2.getChance()).compareTo(Double.valueOf(d1.getChance()))); for (DropHolder dropHolder : dropLists) diff --git a/L2J_Mobius_Classic_2.0_Saviors/java/org/l2jmobius/gameserver/model/actor/templates/NpcTemplate.java b/L2J_Mobius_Classic_2.0_Saviors/java/org/l2jmobius/gameserver/model/actor/templates/NpcTemplate.java index 0f0b5d2cf0..a934208c1a 100644 --- a/L2J_Mobius_Classic_2.0_Saviors/java/org/l2jmobius/gameserver/model/actor/templates/NpcTemplate.java +++ b/L2J_Mobius_Classic_2.0_Saviors/java/org/l2jmobius/gameserver/model/actor/templates/NpcTemplate.java @@ -36,6 +36,7 @@ import org.l2jmobius.gameserver.enums.Race; import org.l2jmobius.gameserver.enums.Sex; import org.l2jmobius.gameserver.model.StatSet; import org.l2jmobius.gameserver.model.actor.Creature; +import org.l2jmobius.gameserver.model.holders.DropGroupHolder; import org.l2jmobius.gameserver.model.holders.DropHolder; import org.l2jmobius.gameserver.model.holders.ItemHolder; import org.l2jmobius.gameserver.model.interfaces.IIdentifiable; @@ -108,6 +109,7 @@ public class NpcTemplate extends CreatureTemplate implements IIdentifiable private Map> _aiSkillLists; private Set _clans; private Set _ignoreClanNpcIds; + private List _dropGroups; private List _dropListDeath; private List _dropListSpoil; private float _collisionRadiusGrown; @@ -650,12 +652,22 @@ public class NpcTemplate extends CreatureTemplate implements IIdentifiable _ignoreClanNpcIds = ignoreClanNpcIds != null ? Collections.unmodifiableSet(ignoreClanNpcIds) : null; } + public void removeDropGroups() + { + _dropGroups = null; + } + public void removeDrops() { _dropListDeath = null; _dropListSpoil = null; } + public void setDropGroups(List groups) + { + _dropGroups = groups; + } + public void addDrop(DropHolder dropHolder) { if (_dropListDeath == null) @@ -674,6 +686,11 @@ public class NpcTemplate extends CreatureTemplate implements IIdentifiable _dropListSpoil.add(dropHolder); } + public List getDropGroups() + { + return _dropGroups; + } + public List getDropList() { return _dropListDeath; @@ -686,11 +703,201 @@ public class NpcTemplate extends CreatureTemplate implements IIdentifiable public List calculateDrops(DropType dropType, Creature victim, Creature killer) { - final List dropList = dropType == DropType.SPOIL ? _dropListSpoil : _dropListDeath; - if (dropList == null) + if (dropType == DropType.DROP) { - return null; + // calculate group drops + List groupDrops = null; + if (_dropGroups != null) + { + groupDrops = calculateGroupDrops(victim, killer); + } + + // calculate ungrouped drops + List ungroupedDrops = null; + if (_dropListDeath != null) + { + ungroupedDrops = calculateUngroupedDrops(dropType, victim, killer); + } + + // return results + if ((groupDrops != null) && (ungroupedDrops != null)) + { + groupDrops.addAll(ungroupedDrops); + ungroupedDrops.clear(); + return groupDrops; + } + if (groupDrops != null) + { + return groupDrops; + } + if (ungroupedDrops != null) + { + return ungroupedDrops; + } } + else if ((dropType == DropType.SPOIL) && (_dropListSpoil != null)) + { + return calculateUngroupedDrops(dropType, victim, killer); + } + + // no drops + return null; + } + + private List calculateGroupDrops(Creature victim, Creature killer) + { + // level difference calculations + final int levelDifference = victim.getLevel() - killer.getLevel(); + final double levelGapChanceToDropAdena = Util.map(levelDifference, -Config.DROP_ADENA_MAX_LEVEL_DIFFERENCE, -Config.DROP_ADENA_MIN_LEVEL_DIFFERENCE, Config.DROP_ADENA_MIN_LEVEL_GAP_CHANCE, 100d); + final double levelGapChanceToDrop = Util.map(levelDifference, -Config.DROP_ITEM_MAX_LEVEL_DIFFERENCE, -Config.DROP_ITEM_MIN_LEVEL_DIFFERENCE, Config.DROP_ITEM_MIN_LEVEL_GAP_CHANCE, 100d); + + int dropOccurrenceCounter = victim.isRaid() ? Config.DROP_MAX_OCCURRENCES_RAIDBOSS : Config.DROP_MAX_OCCURRENCES_NORMAL; + List calculatedDrops = null; + for (DropGroupHolder group : _dropGroups) + { + if (dropOccurrenceCounter <= 0) + { + break; + } + + double groupRate = 1; + final List groupDrops = group.getDropList(); + for (DropHolder dropItem : groupDrops) + { + final int itemId = dropItem.getItemId(); + final ItemTemplate item = ItemTable.getInstance().getTemplate(itemId); + final boolean champion = victim.isChampion(); + + // chance + double rateChance = 1; + final Float itemChance = Config.RATE_DROP_CHANCE_BY_ID.get(itemId); + if (itemChance != null) + { + if (itemChance == 0) + { + continue; + } + + rateChance *= itemChance; + if (champion && (itemId == Inventory.ADENA_ID)) + { + rateChance *= Config.CHAMPION_ADENAS_REWARDS_CHANCE; + } + } + else if (item.hasExImmediateEffect()) + { + rateChance *= Config.RATE_HERB_DROP_CHANCE_MULTIPLIER; + } + else if (victim.isRaid()) + { + rateChance *= Config.RATE_RAID_DROP_CHANCE_MULTIPLIER; + } + else + { + rateChance *= Config.RATE_DEATH_DROP_CHANCE_MULTIPLIER * (champion ? Config.CHAMPION_REWARDS_CHANCE : 1); + } + + // premium chance + if (Config.PREMIUM_SYSTEM_ENABLED && (killer.getActingPlayer() != null) && killer.getActingPlayer().hasPremiumStatus()) + { + if (Config.PREMIUM_RATE_DROP_CHANCE_BY_ID.get(itemId) != null) + { + rateChance *= Config.PREMIUM_RATE_DROP_CHANCE_BY_ID.get(itemId); + } + else if (item.hasExImmediateEffect()) + { + // TODO: Premium herb chance? :) + } + else if (victim.isRaid()) + { + // TODO: Premium raid chance? :) + } + else + { + rateChance *= Config.PREMIUM_RATE_DROP_CHANCE; + } + } + + // keep lowest to avoid chance by id configuration conflicts + groupRate = Math.min(groupRate, rateChance); + } + + if ((Rnd.nextDouble() * 100) < (group.getChance() * groupRate)) + { + double totalChance = 0; // total group chance is 100 + final double dropChance = Rnd.nextDouble() * 100; + GROUP_DROP: for (DropHolder dropItem : groupDrops) + { + if (dropOccurrenceCounter <= 0) + { + break; + } + + // calculate if item will drop + totalChance += dropItem.getChance(); + if (dropChance >= totalChance) + { + continue; + } + + // check level gap that may prevent to drop item + if ((Rnd.nextDouble() * 100) > (dropItem.getItemId() == Inventory.ADENA_ID ? levelGapChanceToDropAdena : levelGapChanceToDrop)) + { + continue; + } + + // create the drop + final ItemHolder drop = calculateGroupDrop(dropItem, victim, killer); + + // create list + if (calculatedDrops == null) + { + calculatedDrops = new ArrayList<>(dropOccurrenceCounter); + } + + // finally + if (group.getChance() < 100) + { + dropOccurrenceCounter--; + } + calculatedDrops.add(drop); + + // no more drops from this group + break GROUP_DROP; + } + } + } + + // champion extra drop + if (victim.isChampion()) + { + if ((victim.getLevel() < killer.getLevel()) && (Rnd.get(100) < Config.CHAMPION_REWARD_LOWER_LEVEL_ITEM_CHANCE)) + { + return calculatedDrops; + } + if ((victim.getLevel() > killer.getLevel()) && (Rnd.get(100) < Config.CHAMPION_REWARD_HIGHER_LEVEL_ITEM_CHANCE)) + { + return calculatedDrops; + } + + // create list + if (calculatedDrops == null) + { + calculatedDrops = new ArrayList<>(); + } + + if (!calculatedDrops.containsAll(Config.CHAMPION_REWARD_ITEMS)) + { + calculatedDrops.addAll(Config.CHAMPION_REWARD_ITEMS); + } + } + + return calculatedDrops; + } + + private List calculateUngroupedDrops(DropType dropType, Creature victim, Creature killer) + { + final List dropList = dropType == DropType.SPOIL ? _dropListSpoil : _dropListDeath; // level difference calculations final int levelDifference = victim.getLevel() - killer.getLevel(); @@ -722,7 +929,7 @@ public class NpcTemplate extends CreatureTemplate implements IIdentifiable } // calculate chances - final ItemHolder drop = calculateDrop(dropItem, victim, killer); + final ItemHolder drop = calculateUngroupedDrop(dropItem, victim, killer); if (drop == null) { continue; @@ -777,7 +984,10 @@ public class NpcTemplate extends CreatureTemplate implements IIdentifiable calculatedDrops = new ArrayList<>(); } - calculatedDrops.addAll(Config.CHAMPION_REWARD_ITEMS); + if (!calculatedDrops.containsAll(Config.CHAMPION_REWARD_ITEMS)) + { + calculatedDrops.addAll(Config.CHAMPION_REWARD_ITEMS); + } } if (Config.VIP_SYSTEM_ENABLED && (dropType == DropType.DROP)) @@ -840,7 +1050,7 @@ public class NpcTemplate extends CreatureTemplate implements IIdentifiable { return null; } - return calculateDrop(dropItem, victim, killer); + return calculateUngroupedDrop(dropItem, victim, killer); } private double calculateLevelGapChanceToDrop(DropHolder dropItem, int levelDifference) @@ -858,13 +1068,72 @@ public class NpcTemplate extends CreatureTemplate implements IIdentifiable } /** - * All item drop chance calculations are done by this method. * @param dropItem * @param victim * @param killer * @return ItemHolder */ - private ItemHolder calculateDrop(DropHolder dropItem, Creature victim, Creature killer) + private ItemHolder calculateGroupDrop(DropHolder dropItem, Creature victim, Creature killer) + { + final int itemId = dropItem.getItemId(); + final ItemTemplate item = ItemTable.getInstance().getTemplate(itemId); + final boolean champion = victim.isChampion(); + + // calculate amount + double rateAmount = 1; + if (Config.RATE_DROP_AMOUNT_BY_ID.get(itemId) != null) + { + rateAmount *= Config.RATE_DROP_AMOUNT_BY_ID.get(itemId); + if (champion && (itemId == Inventory.ADENA_ID)) + { + rateAmount *= Config.CHAMPION_ADENAS_REWARDS_AMOUNT; + } + } + else if (item.hasExImmediateEffect()) + { + rateAmount *= Config.RATE_HERB_DROP_AMOUNT_MULTIPLIER; + } + else if (victim.isRaid()) + { + rateAmount *= Config.RATE_RAID_DROP_AMOUNT_MULTIPLIER; + } + else + { + rateAmount *= Config.RATE_DEATH_DROP_AMOUNT_MULTIPLIER * (champion ? Config.CHAMPION_REWARDS_AMOUNT : 1); + } + + // premium amount + if (Config.PREMIUM_SYSTEM_ENABLED && (killer.getActingPlayer() != null) && killer.getActingPlayer().hasPremiumStatus()) + { + if (Config.PREMIUM_RATE_DROP_AMOUNT_BY_ID.get(itemId) != null) + { + rateAmount *= Config.PREMIUM_RATE_DROP_AMOUNT_BY_ID.get(itemId); + } + else if (item.hasExImmediateEffect()) + { + // TODO: Premium herb amount? :) + } + else if (victim.isRaid()) + { + // TODO: Premium raid amount? :) + } + else + { + rateAmount *= Config.PREMIUM_RATE_DROP_AMOUNT; + } + } + + // finally + return new ItemHolder(itemId, (long) (Rnd.get(dropItem.getMin(), dropItem.getMax()) * rateAmount)); + } + + /** + * @param dropItem + * @param victim + * @param killer + * @return ItemHolder + */ + private ItemHolder calculateUngroupedDrop(DropHolder dropItem, Creature victim, Creature killer) { switch (dropItem.getDropType()) { diff --git a/L2J_Mobius_Classic_2.0_Saviors/java/org/l2jmobius/gameserver/model/holders/DropGroupHolder.java b/L2J_Mobius_Classic_2.0_Saviors/java/org/l2jmobius/gameserver/model/holders/DropGroupHolder.java new file mode 100644 index 0000000000..6c2401c0b0 --- /dev/null +++ b/L2J_Mobius_Classic_2.0_Saviors/java/org/l2jmobius/gameserver/model/holders/DropGroupHolder.java @@ -0,0 +1,49 @@ +/* + * This file is part of the L2J Mobius project. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package org.l2jmobius.gameserver.model.holders; + +import java.util.ArrayList; +import java.util.List; + +/** + * @author Mobius + */ +public class DropGroupHolder +{ + private final List _dropList = new ArrayList<>(); + private final double _chance; + + public DropGroupHolder(double chance) + { + _chance = chance; + } + + public List getDropList() + { + return _dropList; + } + + public void addDrop(DropHolder holder) + { + _dropList.add(holder); + } + + public double getChance() + { + return _chance; + } +} diff --git a/L2J_Mobius_Classic_2.1_Zaken/dist/game/data/scripts/handlers/bypasshandlers/NpcViewMod.java b/L2J_Mobius_Classic_2.1_Zaken/dist/game/data/scripts/handlers/bypasshandlers/NpcViewMod.java index 2e5b2846bf..38ca23ffc7 100644 --- a/L2J_Mobius_Classic_2.1_Zaken/dist/game/data/scripts/handlers/bypasshandlers/NpcViewMod.java +++ b/L2J_Mobius_Classic_2.1_Zaken/dist/game/data/scripts/handlers/bypasshandlers/NpcViewMod.java @@ -37,6 +37,7 @@ import org.l2jmobius.gameserver.model.actor.Attackable; import org.l2jmobius.gameserver.model.actor.Creature; import org.l2jmobius.gameserver.model.actor.Npc; import org.l2jmobius.gameserver.model.actor.Player; +import org.l2jmobius.gameserver.model.holders.DropGroupHolder; import org.l2jmobius.gameserver.model.holders.DropHolder; import org.l2jmobius.gameserver.model.item.ItemTemplate; import org.l2jmobius.gameserver.model.itemcontainer.Inventory; @@ -326,12 +327,13 @@ public class NpcViewMod implements IBypassHandler private static String getDropListButtons(Npc npc) { final StringBuilder sb = new StringBuilder(); + final List dropListGroups = npc.getTemplate().getDropGroups(); final List dropListDeath = npc.getTemplate().getDropList(); final List dropListSpoil = npc.getTemplate().getSpoilList(); - if ((dropListDeath != null) || (dropListSpoil != null)) + if ((dropListGroups != null) || (dropListDeath != null) || (dropListSpoil != null)) { sb.append("
"); - if (dropListDeath != null) + if ((dropListGroups != null) || (dropListDeath != null)) { sb.append(""); } @@ -348,13 +350,40 @@ public class NpcViewMod implements IBypassHandler private void sendNpcDropList(Player player, Npc npc, DropType dropType, int pageValue) { - final List templateList = dropType == DropType.SPOIL ? npc.getTemplate().getSpoilList() : npc.getTemplate().getDropList(); - if (templateList == null) + List dropList = null; + if (dropType == DropType.SPOIL) + { + dropList = new ArrayList<>(npc.getTemplate().getSpoilList()); + } + else + { + final List drops = npc.getTemplate().getDropList(); + if (drops != null) + { + dropList = new ArrayList<>(drops); + } + final List dropGroups = npc.getTemplate().getDropGroups(); + if (dropGroups != null) + { + if (dropList == null) + { + dropList = new ArrayList<>(); + } + for (DropGroupHolder dropGroup : dropGroups) + { + final double chance = dropGroup.getChance() / 100; + for (DropHolder dropHolder : dropGroup.getDropList()) + { + dropList.add(new DropHolder(dropHolder.getDropType(), dropHolder.getItemId(), dropHolder.getMin(), dropHolder.getMax(), dropHolder.getChance() * chance)); + } + } + } + } + if (dropList == null) { return; } - final List dropList = new ArrayList<>(templateList); Collections.sort(dropList, (d1, d2) -> Integer.valueOf(d1.getItemId()).compareTo(Integer.valueOf(d2.getItemId()))); int pages = dropList.size() / DROP_LIST_ITEMS_PER_PAGE; diff --git a/L2J_Mobius_Classic_2.1_Zaken/dist/game/data/scripts/handlers/communityboard/DropSearchBoard.java b/L2J_Mobius_Classic_2.1_Zaken/dist/game/data/scripts/handlers/communityboard/DropSearchBoard.java index 647d14cec5..be1d97660b 100644 --- a/L2J_Mobius_Classic_2.1_Zaken/dist/game/data/scripts/handlers/communityboard/DropSearchBoard.java +++ b/L2J_Mobius_Classic_2.1_Zaken/dist/game/data/scripts/handlers/communityboard/DropSearchBoard.java @@ -36,6 +36,7 @@ import org.l2jmobius.gameserver.handler.CommunityBoardHandler; import org.l2jmobius.gameserver.handler.IParseBoardHandler; import org.l2jmobius.gameserver.model.actor.Player; import org.l2jmobius.gameserver.model.actor.templates.NpcTemplate; +import org.l2jmobius.gameserver.model.holders.DropGroupHolder; import org.l2jmobius.gameserver.model.holders.DropHolder; import org.l2jmobius.gameserver.model.item.ItemTemplate; import org.l2jmobius.gameserver.model.itemcontainer.Inventory; @@ -104,6 +105,17 @@ public class DropSearchBoard implements IParseBoardHandler private void buildDropIndex() { + NpcData.getInstance().getTemplates(npc -> npc.getDropGroups() != null).forEach(npcTemplate -> + { + for (DropGroupHolder dropGroup : npcTemplate.getDropGroups()) + { + final double chance = dropGroup.getChance() / 100; + for (DropHolder dropHolder : dropGroup.getDropList()) + { + addToDropList(npcTemplate, new DropHolder(dropHolder.getDropType(), dropHolder.getItemId(), dropHolder.getMin(), dropHolder.getMax(), dropHolder.getChance() * chance)); + } + } + }); NpcData.getInstance().getTemplates(npc -> npc.getDropList() != null).forEach(npcTemplate -> { for (DropHolder dropHolder : npcTemplate.getDropList()) diff --git a/L2J_Mobius_Classic_2.1_Zaken/dist/game/data/xsd/npcs.xsd b/L2J_Mobius_Classic_2.1_Zaken/dist/game/data/xsd/npcs.xsd index 117ca4cc1d..2c6e583383 100644 --- a/L2J_Mobius_Classic_2.1_Zaken/dist/game/data/xsd/npcs.xsd +++ b/L2J_Mobius_Classic_2.1_Zaken/dist/game/data/xsd/npcs.xsd @@ -1,16 +1,5 @@ - - - - - - - - - - - @@ -260,11 +249,81 @@ - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/L2J_Mobius_Classic_2.1_Zaken/java/org/l2jmobius/gameserver/data/xml/NpcData.java b/L2J_Mobius_Classic_2.1_Zaken/java/org/l2jmobius/gameserver/data/xml/NpcData.java index 28850ef6bb..d6d5210168 100644 --- a/L2J_Mobius_Classic_2.1_Zaken/java/org/l2jmobius/gameserver/data/xml/NpcData.java +++ b/L2J_Mobius_Classic_2.1_Zaken/java/org/l2jmobius/gameserver/data/xml/NpcData.java @@ -46,6 +46,7 @@ import org.l2jmobius.gameserver.enums.MpRewardType; import org.l2jmobius.gameserver.model.StatSet; import org.l2jmobius.gameserver.model.actor.templates.NpcTemplate; import org.l2jmobius.gameserver.model.effects.EffectType; +import org.l2jmobius.gameserver.model.holders.DropGroupHolder; import org.l2jmobius.gameserver.model.holders.DropHolder; import org.l2jmobius.gameserver.model.skill.Skill; @@ -100,6 +101,7 @@ public class NpcData implements IXmlReader Set clans = null; Set ignoreClanNpcIds = null; List dropLists = null; + List dropGroups = null; set.set("id", npcId); set.set("displayId", parseInteger(attrs, "displayId")); set.set("level", parseByte(attrs, "level")); @@ -436,24 +438,54 @@ public class NpcData implements IXmlReader if (dropType != null) { - if (dropLists == null) - { - dropLists = new ArrayList<>(); - } - for (Node dropNode = dropListsNode.getFirstChild(); dropNode != null; dropNode = dropNode.getNextSibling()) { - final NamedNodeMap dropAttrs = dropNode.getAttributes(); - if ("item".equalsIgnoreCase(dropNode.getNodeName())) + final String nodeName = dropNode.getNodeName(); + if (nodeName.equalsIgnoreCase("group")) { - final DropHolder dropItem = new DropHolder(dropType, parseInteger(dropAttrs, "id"), parseLong(dropAttrs, "min"), parseLong(dropAttrs, "max"), parseDouble(dropAttrs, "chance")); - if (ItemTable.getInstance().getTemplate(parseInteger(dropAttrs, "id")) == null) + if (dropGroups == null) { - LOGGER.warning("DropListItem: Could not find item with id " + parseInteger(dropAttrs, "id") + "."); + dropGroups = new ArrayList<>(); + } + + final DropGroupHolder group = new DropGroupHolder(parseDouble(dropNode.getAttributes(), "chance")); + for (Node groupNode = dropNode.getFirstChild(); groupNode != null; groupNode = groupNode.getNextSibling()) + { + if (groupNode.getNodeName().equalsIgnoreCase("item")) + { + final NamedNodeMap groupAttrs = groupNode.getAttributes(); + final int itemId = parseInteger(groupAttrs, "id"); + + if (ItemTable.getInstance().getTemplate(itemId) == null) + { + LOGGER.warning("DropListItem: Could not find item with id " + itemId + "."); + } + else + { + group.addDrop(new DropHolder(dropType, itemId, parseLong(groupAttrs, "min"), parseLong(groupAttrs, "max"), parseDouble(groupAttrs, "chance"))); + } + } + } + + dropGroups.add(group); + } + else if (nodeName.equalsIgnoreCase("item")) + { + if (dropLists == null) + { + dropLists = new ArrayList<>(); + } + + final NamedNodeMap dropAttrs = dropNode.getAttributes(); + final int itemId = parseInteger(dropAttrs, "id"); + + if (ItemTable.getInstance().getTemplate(itemId) == null) + { + LOGGER.warning("DropListItem: Could not find item with id " + itemId + "."); } else { - dropLists.add(dropItem); + dropLists.add(new DropHolder(dropType, itemId, parseLong(dropAttrs, "min"), parseLong(dropAttrs, "max"), parseDouble(dropAttrs, "chance"))); } } } @@ -612,10 +644,21 @@ public class NpcData implements IXmlReader template.setClans(clans); template.setIgnoreClanNpcIds(ignoreClanNpcIds); + // Clean old drop groups. + template.removeDropGroups(); + + // Set new drop groups. + if (dropGroups != null) + { + template.setDropGroups(dropGroups); + } + + // Clean old drop lists. + template.removeDrops(); + + // Set new drop lists. if (dropLists != null) { - template.removeDrops(); - // Drops are sorted by chance (high to low). Collections.sort(dropLists, (d1, d2) -> Double.valueOf(d2.getChance()).compareTo(Double.valueOf(d1.getChance()))); for (DropHolder dropHolder : dropLists) diff --git a/L2J_Mobius_Classic_2.1_Zaken/java/org/l2jmobius/gameserver/model/actor/templates/NpcTemplate.java b/L2J_Mobius_Classic_2.1_Zaken/java/org/l2jmobius/gameserver/model/actor/templates/NpcTemplate.java index 0f0b5d2cf0..a934208c1a 100644 --- a/L2J_Mobius_Classic_2.1_Zaken/java/org/l2jmobius/gameserver/model/actor/templates/NpcTemplate.java +++ b/L2J_Mobius_Classic_2.1_Zaken/java/org/l2jmobius/gameserver/model/actor/templates/NpcTemplate.java @@ -36,6 +36,7 @@ import org.l2jmobius.gameserver.enums.Race; import org.l2jmobius.gameserver.enums.Sex; import org.l2jmobius.gameserver.model.StatSet; import org.l2jmobius.gameserver.model.actor.Creature; +import org.l2jmobius.gameserver.model.holders.DropGroupHolder; import org.l2jmobius.gameserver.model.holders.DropHolder; import org.l2jmobius.gameserver.model.holders.ItemHolder; import org.l2jmobius.gameserver.model.interfaces.IIdentifiable; @@ -108,6 +109,7 @@ public class NpcTemplate extends CreatureTemplate implements IIdentifiable private Map> _aiSkillLists; private Set _clans; private Set _ignoreClanNpcIds; + private List _dropGroups; private List _dropListDeath; private List _dropListSpoil; private float _collisionRadiusGrown; @@ -650,12 +652,22 @@ public class NpcTemplate extends CreatureTemplate implements IIdentifiable _ignoreClanNpcIds = ignoreClanNpcIds != null ? Collections.unmodifiableSet(ignoreClanNpcIds) : null; } + public void removeDropGroups() + { + _dropGroups = null; + } + public void removeDrops() { _dropListDeath = null; _dropListSpoil = null; } + public void setDropGroups(List groups) + { + _dropGroups = groups; + } + public void addDrop(DropHolder dropHolder) { if (_dropListDeath == null) @@ -674,6 +686,11 @@ public class NpcTemplate extends CreatureTemplate implements IIdentifiable _dropListSpoil.add(dropHolder); } + public List getDropGroups() + { + return _dropGroups; + } + public List getDropList() { return _dropListDeath; @@ -686,11 +703,201 @@ public class NpcTemplate extends CreatureTemplate implements IIdentifiable public List calculateDrops(DropType dropType, Creature victim, Creature killer) { - final List dropList = dropType == DropType.SPOIL ? _dropListSpoil : _dropListDeath; - if (dropList == null) + if (dropType == DropType.DROP) { - return null; + // calculate group drops + List groupDrops = null; + if (_dropGroups != null) + { + groupDrops = calculateGroupDrops(victim, killer); + } + + // calculate ungrouped drops + List ungroupedDrops = null; + if (_dropListDeath != null) + { + ungroupedDrops = calculateUngroupedDrops(dropType, victim, killer); + } + + // return results + if ((groupDrops != null) && (ungroupedDrops != null)) + { + groupDrops.addAll(ungroupedDrops); + ungroupedDrops.clear(); + return groupDrops; + } + if (groupDrops != null) + { + return groupDrops; + } + if (ungroupedDrops != null) + { + return ungroupedDrops; + } } + else if ((dropType == DropType.SPOIL) && (_dropListSpoil != null)) + { + return calculateUngroupedDrops(dropType, victim, killer); + } + + // no drops + return null; + } + + private List calculateGroupDrops(Creature victim, Creature killer) + { + // level difference calculations + final int levelDifference = victim.getLevel() - killer.getLevel(); + final double levelGapChanceToDropAdena = Util.map(levelDifference, -Config.DROP_ADENA_MAX_LEVEL_DIFFERENCE, -Config.DROP_ADENA_MIN_LEVEL_DIFFERENCE, Config.DROP_ADENA_MIN_LEVEL_GAP_CHANCE, 100d); + final double levelGapChanceToDrop = Util.map(levelDifference, -Config.DROP_ITEM_MAX_LEVEL_DIFFERENCE, -Config.DROP_ITEM_MIN_LEVEL_DIFFERENCE, Config.DROP_ITEM_MIN_LEVEL_GAP_CHANCE, 100d); + + int dropOccurrenceCounter = victim.isRaid() ? Config.DROP_MAX_OCCURRENCES_RAIDBOSS : Config.DROP_MAX_OCCURRENCES_NORMAL; + List calculatedDrops = null; + for (DropGroupHolder group : _dropGroups) + { + if (dropOccurrenceCounter <= 0) + { + break; + } + + double groupRate = 1; + final List groupDrops = group.getDropList(); + for (DropHolder dropItem : groupDrops) + { + final int itemId = dropItem.getItemId(); + final ItemTemplate item = ItemTable.getInstance().getTemplate(itemId); + final boolean champion = victim.isChampion(); + + // chance + double rateChance = 1; + final Float itemChance = Config.RATE_DROP_CHANCE_BY_ID.get(itemId); + if (itemChance != null) + { + if (itemChance == 0) + { + continue; + } + + rateChance *= itemChance; + if (champion && (itemId == Inventory.ADENA_ID)) + { + rateChance *= Config.CHAMPION_ADENAS_REWARDS_CHANCE; + } + } + else if (item.hasExImmediateEffect()) + { + rateChance *= Config.RATE_HERB_DROP_CHANCE_MULTIPLIER; + } + else if (victim.isRaid()) + { + rateChance *= Config.RATE_RAID_DROP_CHANCE_MULTIPLIER; + } + else + { + rateChance *= Config.RATE_DEATH_DROP_CHANCE_MULTIPLIER * (champion ? Config.CHAMPION_REWARDS_CHANCE : 1); + } + + // premium chance + if (Config.PREMIUM_SYSTEM_ENABLED && (killer.getActingPlayer() != null) && killer.getActingPlayer().hasPremiumStatus()) + { + if (Config.PREMIUM_RATE_DROP_CHANCE_BY_ID.get(itemId) != null) + { + rateChance *= Config.PREMIUM_RATE_DROP_CHANCE_BY_ID.get(itemId); + } + else if (item.hasExImmediateEffect()) + { + // TODO: Premium herb chance? :) + } + else if (victim.isRaid()) + { + // TODO: Premium raid chance? :) + } + else + { + rateChance *= Config.PREMIUM_RATE_DROP_CHANCE; + } + } + + // keep lowest to avoid chance by id configuration conflicts + groupRate = Math.min(groupRate, rateChance); + } + + if ((Rnd.nextDouble() * 100) < (group.getChance() * groupRate)) + { + double totalChance = 0; // total group chance is 100 + final double dropChance = Rnd.nextDouble() * 100; + GROUP_DROP: for (DropHolder dropItem : groupDrops) + { + if (dropOccurrenceCounter <= 0) + { + break; + } + + // calculate if item will drop + totalChance += dropItem.getChance(); + if (dropChance >= totalChance) + { + continue; + } + + // check level gap that may prevent to drop item + if ((Rnd.nextDouble() * 100) > (dropItem.getItemId() == Inventory.ADENA_ID ? levelGapChanceToDropAdena : levelGapChanceToDrop)) + { + continue; + } + + // create the drop + final ItemHolder drop = calculateGroupDrop(dropItem, victim, killer); + + // create list + if (calculatedDrops == null) + { + calculatedDrops = new ArrayList<>(dropOccurrenceCounter); + } + + // finally + if (group.getChance() < 100) + { + dropOccurrenceCounter--; + } + calculatedDrops.add(drop); + + // no more drops from this group + break GROUP_DROP; + } + } + } + + // champion extra drop + if (victim.isChampion()) + { + if ((victim.getLevel() < killer.getLevel()) && (Rnd.get(100) < Config.CHAMPION_REWARD_LOWER_LEVEL_ITEM_CHANCE)) + { + return calculatedDrops; + } + if ((victim.getLevel() > killer.getLevel()) && (Rnd.get(100) < Config.CHAMPION_REWARD_HIGHER_LEVEL_ITEM_CHANCE)) + { + return calculatedDrops; + } + + // create list + if (calculatedDrops == null) + { + calculatedDrops = new ArrayList<>(); + } + + if (!calculatedDrops.containsAll(Config.CHAMPION_REWARD_ITEMS)) + { + calculatedDrops.addAll(Config.CHAMPION_REWARD_ITEMS); + } + } + + return calculatedDrops; + } + + private List calculateUngroupedDrops(DropType dropType, Creature victim, Creature killer) + { + final List dropList = dropType == DropType.SPOIL ? _dropListSpoil : _dropListDeath; // level difference calculations final int levelDifference = victim.getLevel() - killer.getLevel(); @@ -722,7 +929,7 @@ public class NpcTemplate extends CreatureTemplate implements IIdentifiable } // calculate chances - final ItemHolder drop = calculateDrop(dropItem, victim, killer); + final ItemHolder drop = calculateUngroupedDrop(dropItem, victim, killer); if (drop == null) { continue; @@ -777,7 +984,10 @@ public class NpcTemplate extends CreatureTemplate implements IIdentifiable calculatedDrops = new ArrayList<>(); } - calculatedDrops.addAll(Config.CHAMPION_REWARD_ITEMS); + if (!calculatedDrops.containsAll(Config.CHAMPION_REWARD_ITEMS)) + { + calculatedDrops.addAll(Config.CHAMPION_REWARD_ITEMS); + } } if (Config.VIP_SYSTEM_ENABLED && (dropType == DropType.DROP)) @@ -840,7 +1050,7 @@ public class NpcTemplate extends CreatureTemplate implements IIdentifiable { return null; } - return calculateDrop(dropItem, victim, killer); + return calculateUngroupedDrop(dropItem, victim, killer); } private double calculateLevelGapChanceToDrop(DropHolder dropItem, int levelDifference) @@ -858,13 +1068,72 @@ public class NpcTemplate extends CreatureTemplate implements IIdentifiable } /** - * All item drop chance calculations are done by this method. * @param dropItem * @param victim * @param killer * @return ItemHolder */ - private ItemHolder calculateDrop(DropHolder dropItem, Creature victim, Creature killer) + private ItemHolder calculateGroupDrop(DropHolder dropItem, Creature victim, Creature killer) + { + final int itemId = dropItem.getItemId(); + final ItemTemplate item = ItemTable.getInstance().getTemplate(itemId); + final boolean champion = victim.isChampion(); + + // calculate amount + double rateAmount = 1; + if (Config.RATE_DROP_AMOUNT_BY_ID.get(itemId) != null) + { + rateAmount *= Config.RATE_DROP_AMOUNT_BY_ID.get(itemId); + if (champion && (itemId == Inventory.ADENA_ID)) + { + rateAmount *= Config.CHAMPION_ADENAS_REWARDS_AMOUNT; + } + } + else if (item.hasExImmediateEffect()) + { + rateAmount *= Config.RATE_HERB_DROP_AMOUNT_MULTIPLIER; + } + else if (victim.isRaid()) + { + rateAmount *= Config.RATE_RAID_DROP_AMOUNT_MULTIPLIER; + } + else + { + rateAmount *= Config.RATE_DEATH_DROP_AMOUNT_MULTIPLIER * (champion ? Config.CHAMPION_REWARDS_AMOUNT : 1); + } + + // premium amount + if (Config.PREMIUM_SYSTEM_ENABLED && (killer.getActingPlayer() != null) && killer.getActingPlayer().hasPremiumStatus()) + { + if (Config.PREMIUM_RATE_DROP_AMOUNT_BY_ID.get(itemId) != null) + { + rateAmount *= Config.PREMIUM_RATE_DROP_AMOUNT_BY_ID.get(itemId); + } + else if (item.hasExImmediateEffect()) + { + // TODO: Premium herb amount? :) + } + else if (victim.isRaid()) + { + // TODO: Premium raid amount? :) + } + else + { + rateAmount *= Config.PREMIUM_RATE_DROP_AMOUNT; + } + } + + // finally + return new ItemHolder(itemId, (long) (Rnd.get(dropItem.getMin(), dropItem.getMax()) * rateAmount)); + } + + /** + * @param dropItem + * @param victim + * @param killer + * @return ItemHolder + */ + private ItemHolder calculateUngroupedDrop(DropHolder dropItem, Creature victim, Creature killer) { switch (dropItem.getDropType()) { diff --git a/L2J_Mobius_Classic_2.1_Zaken/java/org/l2jmobius/gameserver/model/holders/DropGroupHolder.java b/L2J_Mobius_Classic_2.1_Zaken/java/org/l2jmobius/gameserver/model/holders/DropGroupHolder.java new file mode 100644 index 0000000000..6c2401c0b0 --- /dev/null +++ b/L2J_Mobius_Classic_2.1_Zaken/java/org/l2jmobius/gameserver/model/holders/DropGroupHolder.java @@ -0,0 +1,49 @@ +/* + * This file is part of the L2J Mobius project. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package org.l2jmobius.gameserver.model.holders; + +import java.util.ArrayList; +import java.util.List; + +/** + * @author Mobius + */ +public class DropGroupHolder +{ + private final List _dropList = new ArrayList<>(); + private final double _chance; + + public DropGroupHolder(double chance) + { + _chance = chance; + } + + public List getDropList() + { + return _dropList; + } + + public void addDrop(DropHolder holder) + { + _dropList.add(holder); + } + + public double getChance() + { + return _chance; + } +} diff --git a/L2J_Mobius_Classic_2.2_Antharas/dist/game/data/scripts/handlers/bypasshandlers/NpcViewMod.java b/L2J_Mobius_Classic_2.2_Antharas/dist/game/data/scripts/handlers/bypasshandlers/NpcViewMod.java index 2e5b2846bf..38ca23ffc7 100644 --- a/L2J_Mobius_Classic_2.2_Antharas/dist/game/data/scripts/handlers/bypasshandlers/NpcViewMod.java +++ b/L2J_Mobius_Classic_2.2_Antharas/dist/game/data/scripts/handlers/bypasshandlers/NpcViewMod.java @@ -37,6 +37,7 @@ import org.l2jmobius.gameserver.model.actor.Attackable; import org.l2jmobius.gameserver.model.actor.Creature; import org.l2jmobius.gameserver.model.actor.Npc; import org.l2jmobius.gameserver.model.actor.Player; +import org.l2jmobius.gameserver.model.holders.DropGroupHolder; import org.l2jmobius.gameserver.model.holders.DropHolder; import org.l2jmobius.gameserver.model.item.ItemTemplate; import org.l2jmobius.gameserver.model.itemcontainer.Inventory; @@ -326,12 +327,13 @@ public class NpcViewMod implements IBypassHandler private static String getDropListButtons(Npc npc) { final StringBuilder sb = new StringBuilder(); + final List dropListGroups = npc.getTemplate().getDropGroups(); final List dropListDeath = npc.getTemplate().getDropList(); final List dropListSpoil = npc.getTemplate().getSpoilList(); - if ((dropListDeath != null) || (dropListSpoil != null)) + if ((dropListGroups != null) || (dropListDeath != null) || (dropListSpoil != null)) { sb.append("
"); - if (dropListDeath != null) + if ((dropListGroups != null) || (dropListDeath != null)) { sb.append(""); } @@ -348,13 +350,40 @@ public class NpcViewMod implements IBypassHandler private void sendNpcDropList(Player player, Npc npc, DropType dropType, int pageValue) { - final List templateList = dropType == DropType.SPOIL ? npc.getTemplate().getSpoilList() : npc.getTemplate().getDropList(); - if (templateList == null) + List dropList = null; + if (dropType == DropType.SPOIL) + { + dropList = new ArrayList<>(npc.getTemplate().getSpoilList()); + } + else + { + final List drops = npc.getTemplate().getDropList(); + if (drops != null) + { + dropList = new ArrayList<>(drops); + } + final List dropGroups = npc.getTemplate().getDropGroups(); + if (dropGroups != null) + { + if (dropList == null) + { + dropList = new ArrayList<>(); + } + for (DropGroupHolder dropGroup : dropGroups) + { + final double chance = dropGroup.getChance() / 100; + for (DropHolder dropHolder : dropGroup.getDropList()) + { + dropList.add(new DropHolder(dropHolder.getDropType(), dropHolder.getItemId(), dropHolder.getMin(), dropHolder.getMax(), dropHolder.getChance() * chance)); + } + } + } + } + if (dropList == null) { return; } - final List dropList = new ArrayList<>(templateList); Collections.sort(dropList, (d1, d2) -> Integer.valueOf(d1.getItemId()).compareTo(Integer.valueOf(d2.getItemId()))); int pages = dropList.size() / DROP_LIST_ITEMS_PER_PAGE; diff --git a/L2J_Mobius_Classic_2.2_Antharas/dist/game/data/scripts/handlers/communityboard/DropSearchBoard.java b/L2J_Mobius_Classic_2.2_Antharas/dist/game/data/scripts/handlers/communityboard/DropSearchBoard.java index 647d14cec5..be1d97660b 100644 --- a/L2J_Mobius_Classic_2.2_Antharas/dist/game/data/scripts/handlers/communityboard/DropSearchBoard.java +++ b/L2J_Mobius_Classic_2.2_Antharas/dist/game/data/scripts/handlers/communityboard/DropSearchBoard.java @@ -36,6 +36,7 @@ import org.l2jmobius.gameserver.handler.CommunityBoardHandler; import org.l2jmobius.gameserver.handler.IParseBoardHandler; import org.l2jmobius.gameserver.model.actor.Player; import org.l2jmobius.gameserver.model.actor.templates.NpcTemplate; +import org.l2jmobius.gameserver.model.holders.DropGroupHolder; import org.l2jmobius.gameserver.model.holders.DropHolder; import org.l2jmobius.gameserver.model.item.ItemTemplate; import org.l2jmobius.gameserver.model.itemcontainer.Inventory; @@ -104,6 +105,17 @@ public class DropSearchBoard implements IParseBoardHandler private void buildDropIndex() { + NpcData.getInstance().getTemplates(npc -> npc.getDropGroups() != null).forEach(npcTemplate -> + { + for (DropGroupHolder dropGroup : npcTemplate.getDropGroups()) + { + final double chance = dropGroup.getChance() / 100; + for (DropHolder dropHolder : dropGroup.getDropList()) + { + addToDropList(npcTemplate, new DropHolder(dropHolder.getDropType(), dropHolder.getItemId(), dropHolder.getMin(), dropHolder.getMax(), dropHolder.getChance() * chance)); + } + } + }); NpcData.getInstance().getTemplates(npc -> npc.getDropList() != null).forEach(npcTemplate -> { for (DropHolder dropHolder : npcTemplate.getDropList()) diff --git a/L2J_Mobius_Classic_2.2_Antharas/dist/game/data/xsd/npcs.xsd b/L2J_Mobius_Classic_2.2_Antharas/dist/game/data/xsd/npcs.xsd index 117ca4cc1d..2c6e583383 100644 --- a/L2J_Mobius_Classic_2.2_Antharas/dist/game/data/xsd/npcs.xsd +++ b/L2J_Mobius_Classic_2.2_Antharas/dist/game/data/xsd/npcs.xsd @@ -1,16 +1,5 @@ - - - - - - - - - - - @@ -260,11 +249,81 @@ - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/L2J_Mobius_Classic_2.2_Antharas/java/org/l2jmobius/gameserver/data/xml/NpcData.java b/L2J_Mobius_Classic_2.2_Antharas/java/org/l2jmobius/gameserver/data/xml/NpcData.java index 28850ef6bb..d6d5210168 100644 --- a/L2J_Mobius_Classic_2.2_Antharas/java/org/l2jmobius/gameserver/data/xml/NpcData.java +++ b/L2J_Mobius_Classic_2.2_Antharas/java/org/l2jmobius/gameserver/data/xml/NpcData.java @@ -46,6 +46,7 @@ import org.l2jmobius.gameserver.enums.MpRewardType; import org.l2jmobius.gameserver.model.StatSet; import org.l2jmobius.gameserver.model.actor.templates.NpcTemplate; import org.l2jmobius.gameserver.model.effects.EffectType; +import org.l2jmobius.gameserver.model.holders.DropGroupHolder; import org.l2jmobius.gameserver.model.holders.DropHolder; import org.l2jmobius.gameserver.model.skill.Skill; @@ -100,6 +101,7 @@ public class NpcData implements IXmlReader Set clans = null; Set ignoreClanNpcIds = null; List dropLists = null; + List dropGroups = null; set.set("id", npcId); set.set("displayId", parseInteger(attrs, "displayId")); set.set("level", parseByte(attrs, "level")); @@ -436,24 +438,54 @@ public class NpcData implements IXmlReader if (dropType != null) { - if (dropLists == null) - { - dropLists = new ArrayList<>(); - } - for (Node dropNode = dropListsNode.getFirstChild(); dropNode != null; dropNode = dropNode.getNextSibling()) { - final NamedNodeMap dropAttrs = dropNode.getAttributes(); - if ("item".equalsIgnoreCase(dropNode.getNodeName())) + final String nodeName = dropNode.getNodeName(); + if (nodeName.equalsIgnoreCase("group")) { - final DropHolder dropItem = new DropHolder(dropType, parseInteger(dropAttrs, "id"), parseLong(dropAttrs, "min"), parseLong(dropAttrs, "max"), parseDouble(dropAttrs, "chance")); - if (ItemTable.getInstance().getTemplate(parseInteger(dropAttrs, "id")) == null) + if (dropGroups == null) { - LOGGER.warning("DropListItem: Could not find item with id " + parseInteger(dropAttrs, "id") + "."); + dropGroups = new ArrayList<>(); + } + + final DropGroupHolder group = new DropGroupHolder(parseDouble(dropNode.getAttributes(), "chance")); + for (Node groupNode = dropNode.getFirstChild(); groupNode != null; groupNode = groupNode.getNextSibling()) + { + if (groupNode.getNodeName().equalsIgnoreCase("item")) + { + final NamedNodeMap groupAttrs = groupNode.getAttributes(); + final int itemId = parseInteger(groupAttrs, "id"); + + if (ItemTable.getInstance().getTemplate(itemId) == null) + { + LOGGER.warning("DropListItem: Could not find item with id " + itemId + "."); + } + else + { + group.addDrop(new DropHolder(dropType, itemId, parseLong(groupAttrs, "min"), parseLong(groupAttrs, "max"), parseDouble(groupAttrs, "chance"))); + } + } + } + + dropGroups.add(group); + } + else if (nodeName.equalsIgnoreCase("item")) + { + if (dropLists == null) + { + dropLists = new ArrayList<>(); + } + + final NamedNodeMap dropAttrs = dropNode.getAttributes(); + final int itemId = parseInteger(dropAttrs, "id"); + + if (ItemTable.getInstance().getTemplate(itemId) == null) + { + LOGGER.warning("DropListItem: Could not find item with id " + itemId + "."); } else { - dropLists.add(dropItem); + dropLists.add(new DropHolder(dropType, itemId, parseLong(dropAttrs, "min"), parseLong(dropAttrs, "max"), parseDouble(dropAttrs, "chance"))); } } } @@ -612,10 +644,21 @@ public class NpcData implements IXmlReader template.setClans(clans); template.setIgnoreClanNpcIds(ignoreClanNpcIds); + // Clean old drop groups. + template.removeDropGroups(); + + // Set new drop groups. + if (dropGroups != null) + { + template.setDropGroups(dropGroups); + } + + // Clean old drop lists. + template.removeDrops(); + + // Set new drop lists. if (dropLists != null) { - template.removeDrops(); - // Drops are sorted by chance (high to low). Collections.sort(dropLists, (d1, d2) -> Double.valueOf(d2.getChance()).compareTo(Double.valueOf(d1.getChance()))); for (DropHolder dropHolder : dropLists) diff --git a/L2J_Mobius_Classic_2.2_Antharas/java/org/l2jmobius/gameserver/model/actor/templates/NpcTemplate.java b/L2J_Mobius_Classic_2.2_Antharas/java/org/l2jmobius/gameserver/model/actor/templates/NpcTemplate.java index 0f0b5d2cf0..a934208c1a 100644 --- a/L2J_Mobius_Classic_2.2_Antharas/java/org/l2jmobius/gameserver/model/actor/templates/NpcTemplate.java +++ b/L2J_Mobius_Classic_2.2_Antharas/java/org/l2jmobius/gameserver/model/actor/templates/NpcTemplate.java @@ -36,6 +36,7 @@ import org.l2jmobius.gameserver.enums.Race; import org.l2jmobius.gameserver.enums.Sex; import org.l2jmobius.gameserver.model.StatSet; import org.l2jmobius.gameserver.model.actor.Creature; +import org.l2jmobius.gameserver.model.holders.DropGroupHolder; import org.l2jmobius.gameserver.model.holders.DropHolder; import org.l2jmobius.gameserver.model.holders.ItemHolder; import org.l2jmobius.gameserver.model.interfaces.IIdentifiable; @@ -108,6 +109,7 @@ public class NpcTemplate extends CreatureTemplate implements IIdentifiable private Map> _aiSkillLists; private Set _clans; private Set _ignoreClanNpcIds; + private List _dropGroups; private List _dropListDeath; private List _dropListSpoil; private float _collisionRadiusGrown; @@ -650,12 +652,22 @@ public class NpcTemplate extends CreatureTemplate implements IIdentifiable _ignoreClanNpcIds = ignoreClanNpcIds != null ? Collections.unmodifiableSet(ignoreClanNpcIds) : null; } + public void removeDropGroups() + { + _dropGroups = null; + } + public void removeDrops() { _dropListDeath = null; _dropListSpoil = null; } + public void setDropGroups(List groups) + { + _dropGroups = groups; + } + public void addDrop(DropHolder dropHolder) { if (_dropListDeath == null) @@ -674,6 +686,11 @@ public class NpcTemplate extends CreatureTemplate implements IIdentifiable _dropListSpoil.add(dropHolder); } + public List getDropGroups() + { + return _dropGroups; + } + public List getDropList() { return _dropListDeath; @@ -686,11 +703,201 @@ public class NpcTemplate extends CreatureTemplate implements IIdentifiable public List calculateDrops(DropType dropType, Creature victim, Creature killer) { - final List dropList = dropType == DropType.SPOIL ? _dropListSpoil : _dropListDeath; - if (dropList == null) + if (dropType == DropType.DROP) { - return null; + // calculate group drops + List groupDrops = null; + if (_dropGroups != null) + { + groupDrops = calculateGroupDrops(victim, killer); + } + + // calculate ungrouped drops + List ungroupedDrops = null; + if (_dropListDeath != null) + { + ungroupedDrops = calculateUngroupedDrops(dropType, victim, killer); + } + + // return results + if ((groupDrops != null) && (ungroupedDrops != null)) + { + groupDrops.addAll(ungroupedDrops); + ungroupedDrops.clear(); + return groupDrops; + } + if (groupDrops != null) + { + return groupDrops; + } + if (ungroupedDrops != null) + { + return ungroupedDrops; + } } + else if ((dropType == DropType.SPOIL) && (_dropListSpoil != null)) + { + return calculateUngroupedDrops(dropType, victim, killer); + } + + // no drops + return null; + } + + private List calculateGroupDrops(Creature victim, Creature killer) + { + // level difference calculations + final int levelDifference = victim.getLevel() - killer.getLevel(); + final double levelGapChanceToDropAdena = Util.map(levelDifference, -Config.DROP_ADENA_MAX_LEVEL_DIFFERENCE, -Config.DROP_ADENA_MIN_LEVEL_DIFFERENCE, Config.DROP_ADENA_MIN_LEVEL_GAP_CHANCE, 100d); + final double levelGapChanceToDrop = Util.map(levelDifference, -Config.DROP_ITEM_MAX_LEVEL_DIFFERENCE, -Config.DROP_ITEM_MIN_LEVEL_DIFFERENCE, Config.DROP_ITEM_MIN_LEVEL_GAP_CHANCE, 100d); + + int dropOccurrenceCounter = victim.isRaid() ? Config.DROP_MAX_OCCURRENCES_RAIDBOSS : Config.DROP_MAX_OCCURRENCES_NORMAL; + List calculatedDrops = null; + for (DropGroupHolder group : _dropGroups) + { + if (dropOccurrenceCounter <= 0) + { + break; + } + + double groupRate = 1; + final List groupDrops = group.getDropList(); + for (DropHolder dropItem : groupDrops) + { + final int itemId = dropItem.getItemId(); + final ItemTemplate item = ItemTable.getInstance().getTemplate(itemId); + final boolean champion = victim.isChampion(); + + // chance + double rateChance = 1; + final Float itemChance = Config.RATE_DROP_CHANCE_BY_ID.get(itemId); + if (itemChance != null) + { + if (itemChance == 0) + { + continue; + } + + rateChance *= itemChance; + if (champion && (itemId == Inventory.ADENA_ID)) + { + rateChance *= Config.CHAMPION_ADENAS_REWARDS_CHANCE; + } + } + else if (item.hasExImmediateEffect()) + { + rateChance *= Config.RATE_HERB_DROP_CHANCE_MULTIPLIER; + } + else if (victim.isRaid()) + { + rateChance *= Config.RATE_RAID_DROP_CHANCE_MULTIPLIER; + } + else + { + rateChance *= Config.RATE_DEATH_DROP_CHANCE_MULTIPLIER * (champion ? Config.CHAMPION_REWARDS_CHANCE : 1); + } + + // premium chance + if (Config.PREMIUM_SYSTEM_ENABLED && (killer.getActingPlayer() != null) && killer.getActingPlayer().hasPremiumStatus()) + { + if (Config.PREMIUM_RATE_DROP_CHANCE_BY_ID.get(itemId) != null) + { + rateChance *= Config.PREMIUM_RATE_DROP_CHANCE_BY_ID.get(itemId); + } + else if (item.hasExImmediateEffect()) + { + // TODO: Premium herb chance? :) + } + else if (victim.isRaid()) + { + // TODO: Premium raid chance? :) + } + else + { + rateChance *= Config.PREMIUM_RATE_DROP_CHANCE; + } + } + + // keep lowest to avoid chance by id configuration conflicts + groupRate = Math.min(groupRate, rateChance); + } + + if ((Rnd.nextDouble() * 100) < (group.getChance() * groupRate)) + { + double totalChance = 0; // total group chance is 100 + final double dropChance = Rnd.nextDouble() * 100; + GROUP_DROP: for (DropHolder dropItem : groupDrops) + { + if (dropOccurrenceCounter <= 0) + { + break; + } + + // calculate if item will drop + totalChance += dropItem.getChance(); + if (dropChance >= totalChance) + { + continue; + } + + // check level gap that may prevent to drop item + if ((Rnd.nextDouble() * 100) > (dropItem.getItemId() == Inventory.ADENA_ID ? levelGapChanceToDropAdena : levelGapChanceToDrop)) + { + continue; + } + + // create the drop + final ItemHolder drop = calculateGroupDrop(dropItem, victim, killer); + + // create list + if (calculatedDrops == null) + { + calculatedDrops = new ArrayList<>(dropOccurrenceCounter); + } + + // finally + if (group.getChance() < 100) + { + dropOccurrenceCounter--; + } + calculatedDrops.add(drop); + + // no more drops from this group + break GROUP_DROP; + } + } + } + + // champion extra drop + if (victim.isChampion()) + { + if ((victim.getLevel() < killer.getLevel()) && (Rnd.get(100) < Config.CHAMPION_REWARD_LOWER_LEVEL_ITEM_CHANCE)) + { + return calculatedDrops; + } + if ((victim.getLevel() > killer.getLevel()) && (Rnd.get(100) < Config.CHAMPION_REWARD_HIGHER_LEVEL_ITEM_CHANCE)) + { + return calculatedDrops; + } + + // create list + if (calculatedDrops == null) + { + calculatedDrops = new ArrayList<>(); + } + + if (!calculatedDrops.containsAll(Config.CHAMPION_REWARD_ITEMS)) + { + calculatedDrops.addAll(Config.CHAMPION_REWARD_ITEMS); + } + } + + return calculatedDrops; + } + + private List calculateUngroupedDrops(DropType dropType, Creature victim, Creature killer) + { + final List dropList = dropType == DropType.SPOIL ? _dropListSpoil : _dropListDeath; // level difference calculations final int levelDifference = victim.getLevel() - killer.getLevel(); @@ -722,7 +929,7 @@ public class NpcTemplate extends CreatureTemplate implements IIdentifiable } // calculate chances - final ItemHolder drop = calculateDrop(dropItem, victim, killer); + final ItemHolder drop = calculateUngroupedDrop(dropItem, victim, killer); if (drop == null) { continue; @@ -777,7 +984,10 @@ public class NpcTemplate extends CreatureTemplate implements IIdentifiable calculatedDrops = new ArrayList<>(); } - calculatedDrops.addAll(Config.CHAMPION_REWARD_ITEMS); + if (!calculatedDrops.containsAll(Config.CHAMPION_REWARD_ITEMS)) + { + calculatedDrops.addAll(Config.CHAMPION_REWARD_ITEMS); + } } if (Config.VIP_SYSTEM_ENABLED && (dropType == DropType.DROP)) @@ -840,7 +1050,7 @@ public class NpcTemplate extends CreatureTemplate implements IIdentifiable { return null; } - return calculateDrop(dropItem, victim, killer); + return calculateUngroupedDrop(dropItem, victim, killer); } private double calculateLevelGapChanceToDrop(DropHolder dropItem, int levelDifference) @@ -858,13 +1068,72 @@ public class NpcTemplate extends CreatureTemplate implements IIdentifiable } /** - * All item drop chance calculations are done by this method. * @param dropItem * @param victim * @param killer * @return ItemHolder */ - private ItemHolder calculateDrop(DropHolder dropItem, Creature victim, Creature killer) + private ItemHolder calculateGroupDrop(DropHolder dropItem, Creature victim, Creature killer) + { + final int itemId = dropItem.getItemId(); + final ItemTemplate item = ItemTable.getInstance().getTemplate(itemId); + final boolean champion = victim.isChampion(); + + // calculate amount + double rateAmount = 1; + if (Config.RATE_DROP_AMOUNT_BY_ID.get(itemId) != null) + { + rateAmount *= Config.RATE_DROP_AMOUNT_BY_ID.get(itemId); + if (champion && (itemId == Inventory.ADENA_ID)) + { + rateAmount *= Config.CHAMPION_ADENAS_REWARDS_AMOUNT; + } + } + else if (item.hasExImmediateEffect()) + { + rateAmount *= Config.RATE_HERB_DROP_AMOUNT_MULTIPLIER; + } + else if (victim.isRaid()) + { + rateAmount *= Config.RATE_RAID_DROP_AMOUNT_MULTIPLIER; + } + else + { + rateAmount *= Config.RATE_DEATH_DROP_AMOUNT_MULTIPLIER * (champion ? Config.CHAMPION_REWARDS_AMOUNT : 1); + } + + // premium amount + if (Config.PREMIUM_SYSTEM_ENABLED && (killer.getActingPlayer() != null) && killer.getActingPlayer().hasPremiumStatus()) + { + if (Config.PREMIUM_RATE_DROP_AMOUNT_BY_ID.get(itemId) != null) + { + rateAmount *= Config.PREMIUM_RATE_DROP_AMOUNT_BY_ID.get(itemId); + } + else if (item.hasExImmediateEffect()) + { + // TODO: Premium herb amount? :) + } + else if (victim.isRaid()) + { + // TODO: Premium raid amount? :) + } + else + { + rateAmount *= Config.PREMIUM_RATE_DROP_AMOUNT; + } + } + + // finally + return new ItemHolder(itemId, (long) (Rnd.get(dropItem.getMin(), dropItem.getMax()) * rateAmount)); + } + + /** + * @param dropItem + * @param victim + * @param killer + * @return ItemHolder + */ + private ItemHolder calculateUngroupedDrop(DropHolder dropItem, Creature victim, Creature killer) { switch (dropItem.getDropType()) { diff --git a/L2J_Mobius_Classic_2.2_Antharas/java/org/l2jmobius/gameserver/model/holders/DropGroupHolder.java b/L2J_Mobius_Classic_2.2_Antharas/java/org/l2jmobius/gameserver/model/holders/DropGroupHolder.java new file mode 100644 index 0000000000..6c2401c0b0 --- /dev/null +++ b/L2J_Mobius_Classic_2.2_Antharas/java/org/l2jmobius/gameserver/model/holders/DropGroupHolder.java @@ -0,0 +1,49 @@ +/* + * This file is part of the L2J Mobius project. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package org.l2jmobius.gameserver.model.holders; + +import java.util.ArrayList; +import java.util.List; + +/** + * @author Mobius + */ +public class DropGroupHolder +{ + private final List _dropList = new ArrayList<>(); + private final double _chance; + + public DropGroupHolder(double chance) + { + _chance = chance; + } + + public List getDropList() + { + return _dropList; + } + + public void addDrop(DropHolder holder) + { + _dropList.add(holder); + } + + public double getChance() + { + return _chance; + } +} diff --git a/L2J_Mobius_Classic_2.3_SevenSigns/dist/game/data/scripts/handlers/bypasshandlers/NpcViewMod.java b/L2J_Mobius_Classic_2.3_SevenSigns/dist/game/data/scripts/handlers/bypasshandlers/NpcViewMod.java index 2e5b2846bf..38ca23ffc7 100644 --- a/L2J_Mobius_Classic_2.3_SevenSigns/dist/game/data/scripts/handlers/bypasshandlers/NpcViewMod.java +++ b/L2J_Mobius_Classic_2.3_SevenSigns/dist/game/data/scripts/handlers/bypasshandlers/NpcViewMod.java @@ -37,6 +37,7 @@ import org.l2jmobius.gameserver.model.actor.Attackable; import org.l2jmobius.gameserver.model.actor.Creature; import org.l2jmobius.gameserver.model.actor.Npc; import org.l2jmobius.gameserver.model.actor.Player; +import org.l2jmobius.gameserver.model.holders.DropGroupHolder; import org.l2jmobius.gameserver.model.holders.DropHolder; import org.l2jmobius.gameserver.model.item.ItemTemplate; import org.l2jmobius.gameserver.model.itemcontainer.Inventory; @@ -326,12 +327,13 @@ public class NpcViewMod implements IBypassHandler private static String getDropListButtons(Npc npc) { final StringBuilder sb = new StringBuilder(); + final List dropListGroups = npc.getTemplate().getDropGroups(); final List dropListDeath = npc.getTemplate().getDropList(); final List dropListSpoil = npc.getTemplate().getSpoilList(); - if ((dropListDeath != null) || (dropListSpoil != null)) + if ((dropListGroups != null) || (dropListDeath != null) || (dropListSpoil != null)) { sb.append("
"); - if (dropListDeath != null) + if ((dropListGroups != null) || (dropListDeath != null)) { sb.append(""); } @@ -348,13 +350,40 @@ public class NpcViewMod implements IBypassHandler private void sendNpcDropList(Player player, Npc npc, DropType dropType, int pageValue) { - final List templateList = dropType == DropType.SPOIL ? npc.getTemplate().getSpoilList() : npc.getTemplate().getDropList(); - if (templateList == null) + List dropList = null; + if (dropType == DropType.SPOIL) + { + dropList = new ArrayList<>(npc.getTemplate().getSpoilList()); + } + else + { + final List drops = npc.getTemplate().getDropList(); + if (drops != null) + { + dropList = new ArrayList<>(drops); + } + final List dropGroups = npc.getTemplate().getDropGroups(); + if (dropGroups != null) + { + if (dropList == null) + { + dropList = new ArrayList<>(); + } + for (DropGroupHolder dropGroup : dropGroups) + { + final double chance = dropGroup.getChance() / 100; + for (DropHolder dropHolder : dropGroup.getDropList()) + { + dropList.add(new DropHolder(dropHolder.getDropType(), dropHolder.getItemId(), dropHolder.getMin(), dropHolder.getMax(), dropHolder.getChance() * chance)); + } + } + } + } + if (dropList == null) { return; } - final List dropList = new ArrayList<>(templateList); Collections.sort(dropList, (d1, d2) -> Integer.valueOf(d1.getItemId()).compareTo(Integer.valueOf(d2.getItemId()))); int pages = dropList.size() / DROP_LIST_ITEMS_PER_PAGE; diff --git a/L2J_Mobius_Classic_2.3_SevenSigns/dist/game/data/scripts/handlers/communityboard/DropSearchBoard.java b/L2J_Mobius_Classic_2.3_SevenSigns/dist/game/data/scripts/handlers/communityboard/DropSearchBoard.java index 647d14cec5..be1d97660b 100644 --- a/L2J_Mobius_Classic_2.3_SevenSigns/dist/game/data/scripts/handlers/communityboard/DropSearchBoard.java +++ b/L2J_Mobius_Classic_2.3_SevenSigns/dist/game/data/scripts/handlers/communityboard/DropSearchBoard.java @@ -36,6 +36,7 @@ import org.l2jmobius.gameserver.handler.CommunityBoardHandler; import org.l2jmobius.gameserver.handler.IParseBoardHandler; import org.l2jmobius.gameserver.model.actor.Player; import org.l2jmobius.gameserver.model.actor.templates.NpcTemplate; +import org.l2jmobius.gameserver.model.holders.DropGroupHolder; import org.l2jmobius.gameserver.model.holders.DropHolder; import org.l2jmobius.gameserver.model.item.ItemTemplate; import org.l2jmobius.gameserver.model.itemcontainer.Inventory; @@ -104,6 +105,17 @@ public class DropSearchBoard implements IParseBoardHandler private void buildDropIndex() { + NpcData.getInstance().getTemplates(npc -> npc.getDropGroups() != null).forEach(npcTemplate -> + { + for (DropGroupHolder dropGroup : npcTemplate.getDropGroups()) + { + final double chance = dropGroup.getChance() / 100; + for (DropHolder dropHolder : dropGroup.getDropList()) + { + addToDropList(npcTemplate, new DropHolder(dropHolder.getDropType(), dropHolder.getItemId(), dropHolder.getMin(), dropHolder.getMax(), dropHolder.getChance() * chance)); + } + } + }); NpcData.getInstance().getTemplates(npc -> npc.getDropList() != null).forEach(npcTemplate -> { for (DropHolder dropHolder : npcTemplate.getDropList()) diff --git a/L2J_Mobius_Classic_2.3_SevenSigns/dist/game/data/xsd/npcs.xsd b/L2J_Mobius_Classic_2.3_SevenSigns/dist/game/data/xsd/npcs.xsd index ac78c2d82f..e252403853 100644 --- a/L2J_Mobius_Classic_2.3_SevenSigns/dist/game/data/xsd/npcs.xsd +++ b/L2J_Mobius_Classic_2.3_SevenSigns/dist/game/data/xsd/npcs.xsd @@ -1,16 +1,5 @@ - - - - - - - - - - - @@ -261,11 +250,81 @@ - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/L2J_Mobius_Classic_2.3_SevenSigns/java/org/l2jmobius/gameserver/data/xml/NpcData.java b/L2J_Mobius_Classic_2.3_SevenSigns/java/org/l2jmobius/gameserver/data/xml/NpcData.java index a09a0bb79f..838d3899d0 100644 --- a/L2J_Mobius_Classic_2.3_SevenSigns/java/org/l2jmobius/gameserver/data/xml/NpcData.java +++ b/L2J_Mobius_Classic_2.3_SevenSigns/java/org/l2jmobius/gameserver/data/xml/NpcData.java @@ -47,6 +47,7 @@ import org.l2jmobius.gameserver.enums.MpRewardType; import org.l2jmobius.gameserver.model.StatSet; import org.l2jmobius.gameserver.model.actor.templates.NpcTemplate; import org.l2jmobius.gameserver.model.effects.EffectType; +import org.l2jmobius.gameserver.model.holders.DropGroupHolder; import org.l2jmobius.gameserver.model.holders.DropHolder; import org.l2jmobius.gameserver.model.skill.Skill; @@ -101,6 +102,7 @@ public class NpcData implements IXmlReader Set clans = null; Set ignoreClanNpcIds = null; List dropLists = null; + List dropGroups = null; set.set("id", npcId); set.set("displayId", parseInteger(attrs, "displayId")); set.set("level", parseByte(attrs, "level")); @@ -439,24 +441,54 @@ public class NpcData implements IXmlReader if (dropType != null) { - if (dropLists == null) - { - dropLists = new ArrayList<>(); - } - for (Node dropNode = dropListsNode.getFirstChild(); dropNode != null; dropNode = dropNode.getNextSibling()) { - final NamedNodeMap dropAttrs = dropNode.getAttributes(); - if ("item".equalsIgnoreCase(dropNode.getNodeName())) + final String nodeName = dropNode.getNodeName(); + if (nodeName.equalsIgnoreCase("group")) { - final DropHolder dropItem = new DropHolder(dropType, parseInteger(dropAttrs, "id"), parseLong(dropAttrs, "min"), parseLong(dropAttrs, "max"), parseDouble(dropAttrs, "chance")); - if (ItemTable.getInstance().getTemplate(parseInteger(dropAttrs, "id")) == null) + if (dropGroups == null) { - LOGGER.warning("DropListItem: Could not find item with id " + parseInteger(dropAttrs, "id") + "."); + dropGroups = new ArrayList<>(); + } + + final DropGroupHolder group = new DropGroupHolder(parseDouble(dropNode.getAttributes(), "chance")); + for (Node groupNode = dropNode.getFirstChild(); groupNode != null; groupNode = groupNode.getNextSibling()) + { + if (groupNode.getNodeName().equalsIgnoreCase("item")) + { + final NamedNodeMap groupAttrs = groupNode.getAttributes(); + final int itemId = parseInteger(groupAttrs, "id"); + + if (ItemTable.getInstance().getTemplate(itemId) == null) + { + LOGGER.warning("DropListItem: Could not find item with id " + itemId + "."); + } + else + { + group.addDrop(new DropHolder(dropType, itemId, parseLong(groupAttrs, "min"), parseLong(groupAttrs, "max"), parseDouble(groupAttrs, "chance"))); + } + } + } + + dropGroups.add(group); + } + else if (nodeName.equalsIgnoreCase("item")) + { + if (dropLists == null) + { + dropLists = new ArrayList<>(); + } + + final NamedNodeMap dropAttrs = dropNode.getAttributes(); + final int itemId = parseInteger(dropAttrs, "id"); + + if (ItemTable.getInstance().getTemplate(itemId) == null) + { + LOGGER.warning("DropListItem: Could not find item with id " + itemId + "."); } else { - dropLists.add(dropItem); + dropLists.add(new DropHolder(dropType, itemId, parseLong(dropAttrs, "min"), parseLong(dropAttrs, "max"), parseDouble(dropAttrs, "chance"))); } } } @@ -615,10 +647,21 @@ public class NpcData implements IXmlReader template.setClans(clans); template.setIgnoreClanNpcIds(ignoreClanNpcIds); + // Clean old drop groups. + template.removeDropGroups(); + + // Set new drop groups. + if (dropGroups != null) + { + template.setDropGroups(dropGroups); + } + + // Clean old drop lists. + template.removeDrops(); + + // Set new drop lists. if (dropLists != null) { - template.removeDrops(); - // Drops are sorted by chance (high to low). Collections.sort(dropLists, (d1, d2) -> Double.valueOf(d2.getChance()).compareTo(Double.valueOf(d1.getChance()))); for (DropHolder dropHolder : dropLists) diff --git a/L2J_Mobius_Classic_2.3_SevenSigns/java/org/l2jmobius/gameserver/model/actor/templates/NpcTemplate.java b/L2J_Mobius_Classic_2.3_SevenSigns/java/org/l2jmobius/gameserver/model/actor/templates/NpcTemplate.java index 94513b66a5..bde4beaba4 100644 --- a/L2J_Mobius_Classic_2.3_SevenSigns/java/org/l2jmobius/gameserver/model/actor/templates/NpcTemplate.java +++ b/L2J_Mobius_Classic_2.3_SevenSigns/java/org/l2jmobius/gameserver/model/actor/templates/NpcTemplate.java @@ -37,6 +37,7 @@ import org.l2jmobius.gameserver.enums.Race; import org.l2jmobius.gameserver.enums.Sex; import org.l2jmobius.gameserver.model.StatSet; import org.l2jmobius.gameserver.model.actor.Creature; +import org.l2jmobius.gameserver.model.holders.DropGroupHolder; import org.l2jmobius.gameserver.model.holders.DropHolder; import org.l2jmobius.gameserver.model.holders.ItemHolder; import org.l2jmobius.gameserver.model.interfaces.IIdentifiable; @@ -109,6 +110,7 @@ public class NpcTemplate extends CreatureTemplate implements IIdentifiable private Map> _aiSkillLists; private Set _clans; private Set _ignoreClanNpcIds; + private List _dropGroups; private List _dropListDeath; private List _dropListSpoil; private float _collisionRadiusGrown; @@ -665,12 +667,22 @@ public class NpcTemplate extends CreatureTemplate implements IIdentifiable _ignoreClanNpcIds = ignoreClanNpcIds != null ? Collections.unmodifiableSet(ignoreClanNpcIds) : null; } + public void removeDropGroups() + { + _dropGroups = null; + } + public void removeDrops() { _dropListDeath = null; _dropListSpoil = null; } + public void setDropGroups(List groups) + { + _dropGroups = groups; + } + public void addDrop(DropHolder dropHolder) { if (_dropListDeath == null) @@ -689,6 +701,11 @@ public class NpcTemplate extends CreatureTemplate implements IIdentifiable _dropListSpoil.add(dropHolder); } + public List getDropGroups() + { + return _dropGroups; + } + public List getDropList() { return _dropListDeath; @@ -701,11 +718,201 @@ public class NpcTemplate extends CreatureTemplate implements IIdentifiable public List calculateDrops(DropType dropType, Creature victim, Creature killer) { - final List dropList = dropType == DropType.SPOIL ? _dropListSpoil : _dropListDeath; - if (dropList == null) + if (dropType == DropType.DROP) { - return null; + // calculate group drops + List groupDrops = null; + if (_dropGroups != null) + { + groupDrops = calculateGroupDrops(victim, killer); + } + + // calculate ungrouped drops + List ungroupedDrops = null; + if (_dropListDeath != null) + { + ungroupedDrops = calculateUngroupedDrops(dropType, victim, killer); + } + + // return results + if ((groupDrops != null) && (ungroupedDrops != null)) + { + groupDrops.addAll(ungroupedDrops); + ungroupedDrops.clear(); + return groupDrops; + } + if (groupDrops != null) + { + return groupDrops; + } + if (ungroupedDrops != null) + { + return ungroupedDrops; + } } + else if ((dropType == DropType.SPOIL) && (_dropListSpoil != null)) + { + return calculateUngroupedDrops(dropType, victim, killer); + } + + // no drops + return null; + } + + private List calculateGroupDrops(Creature victim, Creature killer) + { + // level difference calculations + final int levelDifference = victim.getLevel() - killer.getLevel(); + final double levelGapChanceToDropAdena = Util.map(levelDifference, -Config.DROP_ADENA_MAX_LEVEL_DIFFERENCE, -Config.DROP_ADENA_MIN_LEVEL_DIFFERENCE, Config.DROP_ADENA_MIN_LEVEL_GAP_CHANCE, 100d); + final double levelGapChanceToDrop = Util.map(levelDifference, -Config.DROP_ITEM_MAX_LEVEL_DIFFERENCE, -Config.DROP_ITEM_MIN_LEVEL_DIFFERENCE, Config.DROP_ITEM_MIN_LEVEL_GAP_CHANCE, 100d); + + int dropOccurrenceCounter = victim.isRaid() ? Config.DROP_MAX_OCCURRENCES_RAIDBOSS : Config.DROP_MAX_OCCURRENCES_NORMAL; + List calculatedDrops = null; + for (DropGroupHolder group : _dropGroups) + { + if (dropOccurrenceCounter <= 0) + { + break; + } + + double groupRate = 1; + final List groupDrops = group.getDropList(); + for (DropHolder dropItem : groupDrops) + { + final int itemId = dropItem.getItemId(); + final ItemTemplate item = ItemTable.getInstance().getTemplate(itemId); + final boolean champion = victim.isChampion(); + + // chance + double rateChance = 1; + final Float itemChance = Config.RATE_DROP_CHANCE_BY_ID.get(itemId); + if (itemChance != null) + { + if (itemChance == 0) + { + continue; + } + + rateChance *= itemChance; + if (champion && (itemId == Inventory.ADENA_ID)) + { + rateChance *= Config.CHAMPION_ADENAS_REWARDS_CHANCE; + } + } + else if (item.hasExImmediateEffect()) + { + rateChance *= Config.RATE_HERB_DROP_CHANCE_MULTIPLIER; + } + else if (victim.isRaid()) + { + rateChance *= Config.RATE_RAID_DROP_CHANCE_MULTIPLIER; + } + else + { + rateChance *= Config.RATE_DEATH_DROP_CHANCE_MULTIPLIER * (champion ? Config.CHAMPION_REWARDS_CHANCE : 1); + } + + // premium chance + if (Config.PREMIUM_SYSTEM_ENABLED && (killer.getActingPlayer() != null) && killer.getActingPlayer().hasPremiumStatus()) + { + if (Config.PREMIUM_RATE_DROP_CHANCE_BY_ID.get(itemId) != null) + { + rateChance *= Config.PREMIUM_RATE_DROP_CHANCE_BY_ID.get(itemId); + } + else if (item.hasExImmediateEffect()) + { + // TODO: Premium herb chance? :) + } + else if (victim.isRaid()) + { + // TODO: Premium raid chance? :) + } + else + { + rateChance *= Config.PREMIUM_RATE_DROP_CHANCE; + } + } + + // keep lowest to avoid chance by id configuration conflicts + groupRate = Math.min(groupRate, rateChance); + } + + if ((Rnd.nextDouble() * 100) < (group.getChance() * groupRate)) + { + double totalChance = 0; // total group chance is 100 + final double dropChance = Rnd.nextDouble() * 100; + GROUP_DROP: for (DropHolder dropItem : groupDrops) + { + if (dropOccurrenceCounter <= 0) + { + break; + } + + // calculate if item will drop + totalChance += dropItem.getChance(); + if (dropChance >= totalChance) + { + continue; + } + + // check level gap that may prevent to drop item + if ((Rnd.nextDouble() * 100) > (dropItem.getItemId() == Inventory.ADENA_ID ? levelGapChanceToDropAdena : levelGapChanceToDrop)) + { + continue; + } + + // create the drop + final ItemHolder drop = calculateGroupDrop(dropItem, victim, killer); + + // create list + if (calculatedDrops == null) + { + calculatedDrops = new ArrayList<>(dropOccurrenceCounter); + } + + // finally + if (group.getChance() < 100) + { + dropOccurrenceCounter--; + } + calculatedDrops.add(drop); + + // no more drops from this group + break GROUP_DROP; + } + } + } + + // champion extra drop + if (victim.isChampion()) + { + if ((victim.getLevel() < killer.getLevel()) && (Rnd.get(100) < Config.CHAMPION_REWARD_LOWER_LEVEL_ITEM_CHANCE)) + { + return calculatedDrops; + } + if ((victim.getLevel() > killer.getLevel()) && (Rnd.get(100) < Config.CHAMPION_REWARD_HIGHER_LEVEL_ITEM_CHANCE)) + { + return calculatedDrops; + } + + // create list + if (calculatedDrops == null) + { + calculatedDrops = new ArrayList<>(); + } + + if (!calculatedDrops.containsAll(Config.CHAMPION_REWARD_ITEMS)) + { + calculatedDrops.addAll(Config.CHAMPION_REWARD_ITEMS); + } + } + + return calculatedDrops; + } + + private List calculateUngroupedDrops(DropType dropType, Creature victim, Creature killer) + { + final List dropList = dropType == DropType.SPOIL ? _dropListSpoil : _dropListDeath; // level difference calculations final int levelDifference = victim.getLevel() - killer.getLevel(); @@ -737,7 +944,7 @@ public class NpcTemplate extends CreatureTemplate implements IIdentifiable } // calculate chances - final ItemHolder drop = calculateDrop(dropItem, victim, killer); + final ItemHolder drop = calculateUngroupedDrop(dropItem, victim, killer); if (drop == null) { continue; @@ -792,7 +999,10 @@ public class NpcTemplate extends CreatureTemplate implements IIdentifiable calculatedDrops = new ArrayList<>(); } - calculatedDrops.addAll(Config.CHAMPION_REWARD_ITEMS); + if (!calculatedDrops.containsAll(Config.CHAMPION_REWARD_ITEMS)) + { + calculatedDrops.addAll(Config.CHAMPION_REWARD_ITEMS); + } } if (Config.VIP_SYSTEM_ENABLED && (dropType == DropType.DROP)) @@ -855,7 +1065,7 @@ public class NpcTemplate extends CreatureTemplate implements IIdentifiable { return null; } - return calculateDrop(dropItem, victim, killer); + return calculateUngroupedDrop(dropItem, victim, killer); } private double calculateLevelGapChanceToDrop(DropHolder dropItem, int levelDifference) @@ -873,13 +1083,72 @@ public class NpcTemplate extends CreatureTemplate implements IIdentifiable } /** - * All item drop chance calculations are done by this method. * @param dropItem * @param victim * @param killer * @return ItemHolder */ - private ItemHolder calculateDrop(DropHolder dropItem, Creature victim, Creature killer) + private ItemHolder calculateGroupDrop(DropHolder dropItem, Creature victim, Creature killer) + { + final int itemId = dropItem.getItemId(); + final ItemTemplate item = ItemTable.getInstance().getTemplate(itemId); + final boolean champion = victim.isChampion(); + + // calculate amount + double rateAmount = 1; + if (Config.RATE_DROP_AMOUNT_BY_ID.get(itemId) != null) + { + rateAmount *= Config.RATE_DROP_AMOUNT_BY_ID.get(itemId); + if (champion && (itemId == Inventory.ADENA_ID)) + { + rateAmount *= Config.CHAMPION_ADENAS_REWARDS_AMOUNT; + } + } + else if (item.hasExImmediateEffect()) + { + rateAmount *= Config.RATE_HERB_DROP_AMOUNT_MULTIPLIER; + } + else if (victim.isRaid()) + { + rateAmount *= Config.RATE_RAID_DROP_AMOUNT_MULTIPLIER; + } + else + { + rateAmount *= Config.RATE_DEATH_DROP_AMOUNT_MULTIPLIER * (champion ? Config.CHAMPION_REWARDS_AMOUNT : 1); + } + + // premium amount + if (Config.PREMIUM_SYSTEM_ENABLED && (killer.getActingPlayer() != null) && killer.getActingPlayer().hasPremiumStatus()) + { + if (Config.PREMIUM_RATE_DROP_AMOUNT_BY_ID.get(itemId) != null) + { + rateAmount *= Config.PREMIUM_RATE_DROP_AMOUNT_BY_ID.get(itemId); + } + else if (item.hasExImmediateEffect()) + { + // TODO: Premium herb amount? :) + } + else if (victim.isRaid()) + { + // TODO: Premium raid amount? :) + } + else + { + rateAmount *= Config.PREMIUM_RATE_DROP_AMOUNT; + } + } + + // finally + return new ItemHolder(itemId, (long) (Rnd.get(dropItem.getMin(), dropItem.getMax()) * rateAmount)); + } + + /** + * @param dropItem + * @param victim + * @param killer + * @return ItemHolder + */ + private ItemHolder calculateUngroupedDrop(DropHolder dropItem, Creature victim, Creature killer) { switch (dropItem.getDropType()) { diff --git a/L2J_Mobius_Classic_2.3_SevenSigns/java/org/l2jmobius/gameserver/model/holders/DropGroupHolder.java b/L2J_Mobius_Classic_2.3_SevenSigns/java/org/l2jmobius/gameserver/model/holders/DropGroupHolder.java new file mode 100644 index 0000000000..6c2401c0b0 --- /dev/null +++ b/L2J_Mobius_Classic_2.3_SevenSigns/java/org/l2jmobius/gameserver/model/holders/DropGroupHolder.java @@ -0,0 +1,49 @@ +/* + * This file is part of the L2J Mobius project. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package org.l2jmobius.gameserver.model.holders; + +import java.util.ArrayList; +import java.util.List; + +/** + * @author Mobius + */ +public class DropGroupHolder +{ + private final List _dropList = new ArrayList<>(); + private final double _chance; + + public DropGroupHolder(double chance) + { + _chance = chance; + } + + public List getDropList() + { + return _dropList; + } + + public void addDrop(DropHolder holder) + { + _dropList.add(holder); + } + + public double getChance() + { + return _chance; + } +} diff --git a/L2J_Mobius_Classic_2.4_SecretOfEmpire/dist/game/data/scripts/handlers/bypasshandlers/NpcViewMod.java b/L2J_Mobius_Classic_2.4_SecretOfEmpire/dist/game/data/scripts/handlers/bypasshandlers/NpcViewMod.java index 2e5b2846bf..38ca23ffc7 100644 --- a/L2J_Mobius_Classic_2.4_SecretOfEmpire/dist/game/data/scripts/handlers/bypasshandlers/NpcViewMod.java +++ b/L2J_Mobius_Classic_2.4_SecretOfEmpire/dist/game/data/scripts/handlers/bypasshandlers/NpcViewMod.java @@ -37,6 +37,7 @@ import org.l2jmobius.gameserver.model.actor.Attackable; import org.l2jmobius.gameserver.model.actor.Creature; import org.l2jmobius.gameserver.model.actor.Npc; import org.l2jmobius.gameserver.model.actor.Player; +import org.l2jmobius.gameserver.model.holders.DropGroupHolder; import org.l2jmobius.gameserver.model.holders.DropHolder; import org.l2jmobius.gameserver.model.item.ItemTemplate; import org.l2jmobius.gameserver.model.itemcontainer.Inventory; @@ -326,12 +327,13 @@ public class NpcViewMod implements IBypassHandler private static String getDropListButtons(Npc npc) { final StringBuilder sb = new StringBuilder(); + final List dropListGroups = npc.getTemplate().getDropGroups(); final List dropListDeath = npc.getTemplate().getDropList(); final List dropListSpoil = npc.getTemplate().getSpoilList(); - if ((dropListDeath != null) || (dropListSpoil != null)) + if ((dropListGroups != null) || (dropListDeath != null) || (dropListSpoil != null)) { sb.append("
"); - if (dropListDeath != null) + if ((dropListGroups != null) || (dropListDeath != null)) { sb.append(""); } @@ -348,13 +350,40 @@ public class NpcViewMod implements IBypassHandler private void sendNpcDropList(Player player, Npc npc, DropType dropType, int pageValue) { - final List templateList = dropType == DropType.SPOIL ? npc.getTemplate().getSpoilList() : npc.getTemplate().getDropList(); - if (templateList == null) + List dropList = null; + if (dropType == DropType.SPOIL) + { + dropList = new ArrayList<>(npc.getTemplate().getSpoilList()); + } + else + { + final List drops = npc.getTemplate().getDropList(); + if (drops != null) + { + dropList = new ArrayList<>(drops); + } + final List dropGroups = npc.getTemplate().getDropGroups(); + if (dropGroups != null) + { + if (dropList == null) + { + dropList = new ArrayList<>(); + } + for (DropGroupHolder dropGroup : dropGroups) + { + final double chance = dropGroup.getChance() / 100; + for (DropHolder dropHolder : dropGroup.getDropList()) + { + dropList.add(new DropHolder(dropHolder.getDropType(), dropHolder.getItemId(), dropHolder.getMin(), dropHolder.getMax(), dropHolder.getChance() * chance)); + } + } + } + } + if (dropList == null) { return; } - final List dropList = new ArrayList<>(templateList); Collections.sort(dropList, (d1, d2) -> Integer.valueOf(d1.getItemId()).compareTo(Integer.valueOf(d2.getItemId()))); int pages = dropList.size() / DROP_LIST_ITEMS_PER_PAGE; diff --git a/L2J_Mobius_Classic_2.4_SecretOfEmpire/dist/game/data/scripts/handlers/communityboard/DropSearchBoard.java b/L2J_Mobius_Classic_2.4_SecretOfEmpire/dist/game/data/scripts/handlers/communityboard/DropSearchBoard.java index 647d14cec5..be1d97660b 100644 --- a/L2J_Mobius_Classic_2.4_SecretOfEmpire/dist/game/data/scripts/handlers/communityboard/DropSearchBoard.java +++ b/L2J_Mobius_Classic_2.4_SecretOfEmpire/dist/game/data/scripts/handlers/communityboard/DropSearchBoard.java @@ -36,6 +36,7 @@ import org.l2jmobius.gameserver.handler.CommunityBoardHandler; import org.l2jmobius.gameserver.handler.IParseBoardHandler; import org.l2jmobius.gameserver.model.actor.Player; import org.l2jmobius.gameserver.model.actor.templates.NpcTemplate; +import org.l2jmobius.gameserver.model.holders.DropGroupHolder; import org.l2jmobius.gameserver.model.holders.DropHolder; import org.l2jmobius.gameserver.model.item.ItemTemplate; import org.l2jmobius.gameserver.model.itemcontainer.Inventory; @@ -104,6 +105,17 @@ public class DropSearchBoard implements IParseBoardHandler private void buildDropIndex() { + NpcData.getInstance().getTemplates(npc -> npc.getDropGroups() != null).forEach(npcTemplate -> + { + for (DropGroupHolder dropGroup : npcTemplate.getDropGroups()) + { + final double chance = dropGroup.getChance() / 100; + for (DropHolder dropHolder : dropGroup.getDropList()) + { + addToDropList(npcTemplate, new DropHolder(dropHolder.getDropType(), dropHolder.getItemId(), dropHolder.getMin(), dropHolder.getMax(), dropHolder.getChance() * chance)); + } + } + }); NpcData.getInstance().getTemplates(npc -> npc.getDropList() != null).forEach(npcTemplate -> { for (DropHolder dropHolder : npcTemplate.getDropList()) diff --git a/L2J_Mobius_Classic_2.4_SecretOfEmpire/dist/game/data/xsd/npcs.xsd b/L2J_Mobius_Classic_2.4_SecretOfEmpire/dist/game/data/xsd/npcs.xsd index ac78c2d82f..e252403853 100644 --- a/L2J_Mobius_Classic_2.4_SecretOfEmpire/dist/game/data/xsd/npcs.xsd +++ b/L2J_Mobius_Classic_2.4_SecretOfEmpire/dist/game/data/xsd/npcs.xsd @@ -1,16 +1,5 @@ - - - - - - - - - - - @@ -261,11 +250,81 @@ - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/L2J_Mobius_Classic_2.4_SecretOfEmpire/java/org/l2jmobius/gameserver/data/xml/NpcData.java b/L2J_Mobius_Classic_2.4_SecretOfEmpire/java/org/l2jmobius/gameserver/data/xml/NpcData.java index a09a0bb79f..838d3899d0 100644 --- a/L2J_Mobius_Classic_2.4_SecretOfEmpire/java/org/l2jmobius/gameserver/data/xml/NpcData.java +++ b/L2J_Mobius_Classic_2.4_SecretOfEmpire/java/org/l2jmobius/gameserver/data/xml/NpcData.java @@ -47,6 +47,7 @@ import org.l2jmobius.gameserver.enums.MpRewardType; import org.l2jmobius.gameserver.model.StatSet; import org.l2jmobius.gameserver.model.actor.templates.NpcTemplate; import org.l2jmobius.gameserver.model.effects.EffectType; +import org.l2jmobius.gameserver.model.holders.DropGroupHolder; import org.l2jmobius.gameserver.model.holders.DropHolder; import org.l2jmobius.gameserver.model.skill.Skill; @@ -101,6 +102,7 @@ public class NpcData implements IXmlReader Set clans = null; Set ignoreClanNpcIds = null; List dropLists = null; + List dropGroups = null; set.set("id", npcId); set.set("displayId", parseInteger(attrs, "displayId")); set.set("level", parseByte(attrs, "level")); @@ -439,24 +441,54 @@ public class NpcData implements IXmlReader if (dropType != null) { - if (dropLists == null) - { - dropLists = new ArrayList<>(); - } - for (Node dropNode = dropListsNode.getFirstChild(); dropNode != null; dropNode = dropNode.getNextSibling()) { - final NamedNodeMap dropAttrs = dropNode.getAttributes(); - if ("item".equalsIgnoreCase(dropNode.getNodeName())) + final String nodeName = dropNode.getNodeName(); + if (nodeName.equalsIgnoreCase("group")) { - final DropHolder dropItem = new DropHolder(dropType, parseInteger(dropAttrs, "id"), parseLong(dropAttrs, "min"), parseLong(dropAttrs, "max"), parseDouble(dropAttrs, "chance")); - if (ItemTable.getInstance().getTemplate(parseInteger(dropAttrs, "id")) == null) + if (dropGroups == null) { - LOGGER.warning("DropListItem: Could not find item with id " + parseInteger(dropAttrs, "id") + "."); + dropGroups = new ArrayList<>(); + } + + final DropGroupHolder group = new DropGroupHolder(parseDouble(dropNode.getAttributes(), "chance")); + for (Node groupNode = dropNode.getFirstChild(); groupNode != null; groupNode = groupNode.getNextSibling()) + { + if (groupNode.getNodeName().equalsIgnoreCase("item")) + { + final NamedNodeMap groupAttrs = groupNode.getAttributes(); + final int itemId = parseInteger(groupAttrs, "id"); + + if (ItemTable.getInstance().getTemplate(itemId) == null) + { + LOGGER.warning("DropListItem: Could not find item with id " + itemId + "."); + } + else + { + group.addDrop(new DropHolder(dropType, itemId, parseLong(groupAttrs, "min"), parseLong(groupAttrs, "max"), parseDouble(groupAttrs, "chance"))); + } + } + } + + dropGroups.add(group); + } + else if (nodeName.equalsIgnoreCase("item")) + { + if (dropLists == null) + { + dropLists = new ArrayList<>(); + } + + final NamedNodeMap dropAttrs = dropNode.getAttributes(); + final int itemId = parseInteger(dropAttrs, "id"); + + if (ItemTable.getInstance().getTemplate(itemId) == null) + { + LOGGER.warning("DropListItem: Could not find item with id " + itemId + "."); } else { - dropLists.add(dropItem); + dropLists.add(new DropHolder(dropType, itemId, parseLong(dropAttrs, "min"), parseLong(dropAttrs, "max"), parseDouble(dropAttrs, "chance"))); } } } @@ -615,10 +647,21 @@ public class NpcData implements IXmlReader template.setClans(clans); template.setIgnoreClanNpcIds(ignoreClanNpcIds); + // Clean old drop groups. + template.removeDropGroups(); + + // Set new drop groups. + if (dropGroups != null) + { + template.setDropGroups(dropGroups); + } + + // Clean old drop lists. + template.removeDrops(); + + // Set new drop lists. if (dropLists != null) { - template.removeDrops(); - // Drops are sorted by chance (high to low). Collections.sort(dropLists, (d1, d2) -> Double.valueOf(d2.getChance()).compareTo(Double.valueOf(d1.getChance()))); for (DropHolder dropHolder : dropLists) diff --git a/L2J_Mobius_Classic_2.4_SecretOfEmpire/java/org/l2jmobius/gameserver/model/actor/templates/NpcTemplate.java b/L2J_Mobius_Classic_2.4_SecretOfEmpire/java/org/l2jmobius/gameserver/model/actor/templates/NpcTemplate.java index 94513b66a5..bde4beaba4 100644 --- a/L2J_Mobius_Classic_2.4_SecretOfEmpire/java/org/l2jmobius/gameserver/model/actor/templates/NpcTemplate.java +++ b/L2J_Mobius_Classic_2.4_SecretOfEmpire/java/org/l2jmobius/gameserver/model/actor/templates/NpcTemplate.java @@ -37,6 +37,7 @@ import org.l2jmobius.gameserver.enums.Race; import org.l2jmobius.gameserver.enums.Sex; import org.l2jmobius.gameserver.model.StatSet; import org.l2jmobius.gameserver.model.actor.Creature; +import org.l2jmobius.gameserver.model.holders.DropGroupHolder; import org.l2jmobius.gameserver.model.holders.DropHolder; import org.l2jmobius.gameserver.model.holders.ItemHolder; import org.l2jmobius.gameserver.model.interfaces.IIdentifiable; @@ -109,6 +110,7 @@ public class NpcTemplate extends CreatureTemplate implements IIdentifiable private Map> _aiSkillLists; private Set _clans; private Set _ignoreClanNpcIds; + private List _dropGroups; private List _dropListDeath; private List _dropListSpoil; private float _collisionRadiusGrown; @@ -665,12 +667,22 @@ public class NpcTemplate extends CreatureTemplate implements IIdentifiable _ignoreClanNpcIds = ignoreClanNpcIds != null ? Collections.unmodifiableSet(ignoreClanNpcIds) : null; } + public void removeDropGroups() + { + _dropGroups = null; + } + public void removeDrops() { _dropListDeath = null; _dropListSpoil = null; } + public void setDropGroups(List groups) + { + _dropGroups = groups; + } + public void addDrop(DropHolder dropHolder) { if (_dropListDeath == null) @@ -689,6 +701,11 @@ public class NpcTemplate extends CreatureTemplate implements IIdentifiable _dropListSpoil.add(dropHolder); } + public List getDropGroups() + { + return _dropGroups; + } + public List getDropList() { return _dropListDeath; @@ -701,11 +718,201 @@ public class NpcTemplate extends CreatureTemplate implements IIdentifiable public List calculateDrops(DropType dropType, Creature victim, Creature killer) { - final List dropList = dropType == DropType.SPOIL ? _dropListSpoil : _dropListDeath; - if (dropList == null) + if (dropType == DropType.DROP) { - return null; + // calculate group drops + List groupDrops = null; + if (_dropGroups != null) + { + groupDrops = calculateGroupDrops(victim, killer); + } + + // calculate ungrouped drops + List ungroupedDrops = null; + if (_dropListDeath != null) + { + ungroupedDrops = calculateUngroupedDrops(dropType, victim, killer); + } + + // return results + if ((groupDrops != null) && (ungroupedDrops != null)) + { + groupDrops.addAll(ungroupedDrops); + ungroupedDrops.clear(); + return groupDrops; + } + if (groupDrops != null) + { + return groupDrops; + } + if (ungroupedDrops != null) + { + return ungroupedDrops; + } } + else if ((dropType == DropType.SPOIL) && (_dropListSpoil != null)) + { + return calculateUngroupedDrops(dropType, victim, killer); + } + + // no drops + return null; + } + + private List calculateGroupDrops(Creature victim, Creature killer) + { + // level difference calculations + final int levelDifference = victim.getLevel() - killer.getLevel(); + final double levelGapChanceToDropAdena = Util.map(levelDifference, -Config.DROP_ADENA_MAX_LEVEL_DIFFERENCE, -Config.DROP_ADENA_MIN_LEVEL_DIFFERENCE, Config.DROP_ADENA_MIN_LEVEL_GAP_CHANCE, 100d); + final double levelGapChanceToDrop = Util.map(levelDifference, -Config.DROP_ITEM_MAX_LEVEL_DIFFERENCE, -Config.DROP_ITEM_MIN_LEVEL_DIFFERENCE, Config.DROP_ITEM_MIN_LEVEL_GAP_CHANCE, 100d); + + int dropOccurrenceCounter = victim.isRaid() ? Config.DROP_MAX_OCCURRENCES_RAIDBOSS : Config.DROP_MAX_OCCURRENCES_NORMAL; + List calculatedDrops = null; + for (DropGroupHolder group : _dropGroups) + { + if (dropOccurrenceCounter <= 0) + { + break; + } + + double groupRate = 1; + final List groupDrops = group.getDropList(); + for (DropHolder dropItem : groupDrops) + { + final int itemId = dropItem.getItemId(); + final ItemTemplate item = ItemTable.getInstance().getTemplate(itemId); + final boolean champion = victim.isChampion(); + + // chance + double rateChance = 1; + final Float itemChance = Config.RATE_DROP_CHANCE_BY_ID.get(itemId); + if (itemChance != null) + { + if (itemChance == 0) + { + continue; + } + + rateChance *= itemChance; + if (champion && (itemId == Inventory.ADENA_ID)) + { + rateChance *= Config.CHAMPION_ADENAS_REWARDS_CHANCE; + } + } + else if (item.hasExImmediateEffect()) + { + rateChance *= Config.RATE_HERB_DROP_CHANCE_MULTIPLIER; + } + else if (victim.isRaid()) + { + rateChance *= Config.RATE_RAID_DROP_CHANCE_MULTIPLIER; + } + else + { + rateChance *= Config.RATE_DEATH_DROP_CHANCE_MULTIPLIER * (champion ? Config.CHAMPION_REWARDS_CHANCE : 1); + } + + // premium chance + if (Config.PREMIUM_SYSTEM_ENABLED && (killer.getActingPlayer() != null) && killer.getActingPlayer().hasPremiumStatus()) + { + if (Config.PREMIUM_RATE_DROP_CHANCE_BY_ID.get(itemId) != null) + { + rateChance *= Config.PREMIUM_RATE_DROP_CHANCE_BY_ID.get(itemId); + } + else if (item.hasExImmediateEffect()) + { + // TODO: Premium herb chance? :) + } + else if (victim.isRaid()) + { + // TODO: Premium raid chance? :) + } + else + { + rateChance *= Config.PREMIUM_RATE_DROP_CHANCE; + } + } + + // keep lowest to avoid chance by id configuration conflicts + groupRate = Math.min(groupRate, rateChance); + } + + if ((Rnd.nextDouble() * 100) < (group.getChance() * groupRate)) + { + double totalChance = 0; // total group chance is 100 + final double dropChance = Rnd.nextDouble() * 100; + GROUP_DROP: for (DropHolder dropItem : groupDrops) + { + if (dropOccurrenceCounter <= 0) + { + break; + } + + // calculate if item will drop + totalChance += dropItem.getChance(); + if (dropChance >= totalChance) + { + continue; + } + + // check level gap that may prevent to drop item + if ((Rnd.nextDouble() * 100) > (dropItem.getItemId() == Inventory.ADENA_ID ? levelGapChanceToDropAdena : levelGapChanceToDrop)) + { + continue; + } + + // create the drop + final ItemHolder drop = calculateGroupDrop(dropItem, victim, killer); + + // create list + if (calculatedDrops == null) + { + calculatedDrops = new ArrayList<>(dropOccurrenceCounter); + } + + // finally + if (group.getChance() < 100) + { + dropOccurrenceCounter--; + } + calculatedDrops.add(drop); + + // no more drops from this group + break GROUP_DROP; + } + } + } + + // champion extra drop + if (victim.isChampion()) + { + if ((victim.getLevel() < killer.getLevel()) && (Rnd.get(100) < Config.CHAMPION_REWARD_LOWER_LEVEL_ITEM_CHANCE)) + { + return calculatedDrops; + } + if ((victim.getLevel() > killer.getLevel()) && (Rnd.get(100) < Config.CHAMPION_REWARD_HIGHER_LEVEL_ITEM_CHANCE)) + { + return calculatedDrops; + } + + // create list + if (calculatedDrops == null) + { + calculatedDrops = new ArrayList<>(); + } + + if (!calculatedDrops.containsAll(Config.CHAMPION_REWARD_ITEMS)) + { + calculatedDrops.addAll(Config.CHAMPION_REWARD_ITEMS); + } + } + + return calculatedDrops; + } + + private List calculateUngroupedDrops(DropType dropType, Creature victim, Creature killer) + { + final List dropList = dropType == DropType.SPOIL ? _dropListSpoil : _dropListDeath; // level difference calculations final int levelDifference = victim.getLevel() - killer.getLevel(); @@ -737,7 +944,7 @@ public class NpcTemplate extends CreatureTemplate implements IIdentifiable } // calculate chances - final ItemHolder drop = calculateDrop(dropItem, victim, killer); + final ItemHolder drop = calculateUngroupedDrop(dropItem, victim, killer); if (drop == null) { continue; @@ -792,7 +999,10 @@ public class NpcTemplate extends CreatureTemplate implements IIdentifiable calculatedDrops = new ArrayList<>(); } - calculatedDrops.addAll(Config.CHAMPION_REWARD_ITEMS); + if (!calculatedDrops.containsAll(Config.CHAMPION_REWARD_ITEMS)) + { + calculatedDrops.addAll(Config.CHAMPION_REWARD_ITEMS); + } } if (Config.VIP_SYSTEM_ENABLED && (dropType == DropType.DROP)) @@ -855,7 +1065,7 @@ public class NpcTemplate extends CreatureTemplate implements IIdentifiable { return null; } - return calculateDrop(dropItem, victim, killer); + return calculateUngroupedDrop(dropItem, victim, killer); } private double calculateLevelGapChanceToDrop(DropHolder dropItem, int levelDifference) @@ -873,13 +1083,72 @@ public class NpcTemplate extends CreatureTemplate implements IIdentifiable } /** - * All item drop chance calculations are done by this method. * @param dropItem * @param victim * @param killer * @return ItemHolder */ - private ItemHolder calculateDrop(DropHolder dropItem, Creature victim, Creature killer) + private ItemHolder calculateGroupDrop(DropHolder dropItem, Creature victim, Creature killer) + { + final int itemId = dropItem.getItemId(); + final ItemTemplate item = ItemTable.getInstance().getTemplate(itemId); + final boolean champion = victim.isChampion(); + + // calculate amount + double rateAmount = 1; + if (Config.RATE_DROP_AMOUNT_BY_ID.get(itemId) != null) + { + rateAmount *= Config.RATE_DROP_AMOUNT_BY_ID.get(itemId); + if (champion && (itemId == Inventory.ADENA_ID)) + { + rateAmount *= Config.CHAMPION_ADENAS_REWARDS_AMOUNT; + } + } + else if (item.hasExImmediateEffect()) + { + rateAmount *= Config.RATE_HERB_DROP_AMOUNT_MULTIPLIER; + } + else if (victim.isRaid()) + { + rateAmount *= Config.RATE_RAID_DROP_AMOUNT_MULTIPLIER; + } + else + { + rateAmount *= Config.RATE_DEATH_DROP_AMOUNT_MULTIPLIER * (champion ? Config.CHAMPION_REWARDS_AMOUNT : 1); + } + + // premium amount + if (Config.PREMIUM_SYSTEM_ENABLED && (killer.getActingPlayer() != null) && killer.getActingPlayer().hasPremiumStatus()) + { + if (Config.PREMIUM_RATE_DROP_AMOUNT_BY_ID.get(itemId) != null) + { + rateAmount *= Config.PREMIUM_RATE_DROP_AMOUNT_BY_ID.get(itemId); + } + else if (item.hasExImmediateEffect()) + { + // TODO: Premium herb amount? :) + } + else if (victim.isRaid()) + { + // TODO: Premium raid amount? :) + } + else + { + rateAmount *= Config.PREMIUM_RATE_DROP_AMOUNT; + } + } + + // finally + return new ItemHolder(itemId, (long) (Rnd.get(dropItem.getMin(), dropItem.getMax()) * rateAmount)); + } + + /** + * @param dropItem + * @param victim + * @param killer + * @return ItemHolder + */ + private ItemHolder calculateUngroupedDrop(DropHolder dropItem, Creature victim, Creature killer) { switch (dropItem.getDropType()) { diff --git a/L2J_Mobius_Classic_2.4_SecretOfEmpire/java/org/l2jmobius/gameserver/model/holders/DropGroupHolder.java b/L2J_Mobius_Classic_2.4_SecretOfEmpire/java/org/l2jmobius/gameserver/model/holders/DropGroupHolder.java new file mode 100644 index 0000000000..6c2401c0b0 --- /dev/null +++ b/L2J_Mobius_Classic_2.4_SecretOfEmpire/java/org/l2jmobius/gameserver/model/holders/DropGroupHolder.java @@ -0,0 +1,49 @@ +/* + * This file is part of the L2J Mobius project. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package org.l2jmobius.gameserver.model.holders; + +import java.util.ArrayList; +import java.util.List; + +/** + * @author Mobius + */ +public class DropGroupHolder +{ + private final List _dropList = new ArrayList<>(); + private final double _chance; + + public DropGroupHolder(double chance) + { + _chance = chance; + } + + public List getDropList() + { + return _dropList; + } + + public void addDrop(DropHolder holder) + { + _dropList.add(holder); + } + + public double getChance() + { + return _chance; + } +} diff --git a/L2J_Mobius_Classic_3.0_TheKamael/dist/game/data/scripts/handlers/bypasshandlers/NpcViewMod.java b/L2J_Mobius_Classic_3.0_TheKamael/dist/game/data/scripts/handlers/bypasshandlers/NpcViewMod.java index 2e5b2846bf..38ca23ffc7 100644 --- a/L2J_Mobius_Classic_3.0_TheKamael/dist/game/data/scripts/handlers/bypasshandlers/NpcViewMod.java +++ b/L2J_Mobius_Classic_3.0_TheKamael/dist/game/data/scripts/handlers/bypasshandlers/NpcViewMod.java @@ -37,6 +37,7 @@ import org.l2jmobius.gameserver.model.actor.Attackable; import org.l2jmobius.gameserver.model.actor.Creature; import org.l2jmobius.gameserver.model.actor.Npc; import org.l2jmobius.gameserver.model.actor.Player; +import org.l2jmobius.gameserver.model.holders.DropGroupHolder; import org.l2jmobius.gameserver.model.holders.DropHolder; import org.l2jmobius.gameserver.model.item.ItemTemplate; import org.l2jmobius.gameserver.model.itemcontainer.Inventory; @@ -326,12 +327,13 @@ public class NpcViewMod implements IBypassHandler private static String getDropListButtons(Npc npc) { final StringBuilder sb = new StringBuilder(); + final List dropListGroups = npc.getTemplate().getDropGroups(); final List dropListDeath = npc.getTemplate().getDropList(); final List dropListSpoil = npc.getTemplate().getSpoilList(); - if ((dropListDeath != null) || (dropListSpoil != null)) + if ((dropListGroups != null) || (dropListDeath != null) || (dropListSpoil != null)) { sb.append("
"); - if (dropListDeath != null) + if ((dropListGroups != null) || (dropListDeath != null)) { sb.append(""); } @@ -348,13 +350,40 @@ public class NpcViewMod implements IBypassHandler private void sendNpcDropList(Player player, Npc npc, DropType dropType, int pageValue) { - final List templateList = dropType == DropType.SPOIL ? npc.getTemplate().getSpoilList() : npc.getTemplate().getDropList(); - if (templateList == null) + List dropList = null; + if (dropType == DropType.SPOIL) + { + dropList = new ArrayList<>(npc.getTemplate().getSpoilList()); + } + else + { + final List drops = npc.getTemplate().getDropList(); + if (drops != null) + { + dropList = new ArrayList<>(drops); + } + final List dropGroups = npc.getTemplate().getDropGroups(); + if (dropGroups != null) + { + if (dropList == null) + { + dropList = new ArrayList<>(); + } + for (DropGroupHolder dropGroup : dropGroups) + { + final double chance = dropGroup.getChance() / 100; + for (DropHolder dropHolder : dropGroup.getDropList()) + { + dropList.add(new DropHolder(dropHolder.getDropType(), dropHolder.getItemId(), dropHolder.getMin(), dropHolder.getMax(), dropHolder.getChance() * chance)); + } + } + } + } + if (dropList == null) { return; } - final List dropList = new ArrayList<>(templateList); Collections.sort(dropList, (d1, d2) -> Integer.valueOf(d1.getItemId()).compareTo(Integer.valueOf(d2.getItemId()))); int pages = dropList.size() / DROP_LIST_ITEMS_PER_PAGE; diff --git a/L2J_Mobius_Classic_3.0_TheKamael/dist/game/data/scripts/handlers/communityboard/DropSearchBoard.java b/L2J_Mobius_Classic_3.0_TheKamael/dist/game/data/scripts/handlers/communityboard/DropSearchBoard.java index 647d14cec5..be1d97660b 100644 --- a/L2J_Mobius_Classic_3.0_TheKamael/dist/game/data/scripts/handlers/communityboard/DropSearchBoard.java +++ b/L2J_Mobius_Classic_3.0_TheKamael/dist/game/data/scripts/handlers/communityboard/DropSearchBoard.java @@ -36,6 +36,7 @@ import org.l2jmobius.gameserver.handler.CommunityBoardHandler; import org.l2jmobius.gameserver.handler.IParseBoardHandler; import org.l2jmobius.gameserver.model.actor.Player; import org.l2jmobius.gameserver.model.actor.templates.NpcTemplate; +import org.l2jmobius.gameserver.model.holders.DropGroupHolder; import org.l2jmobius.gameserver.model.holders.DropHolder; import org.l2jmobius.gameserver.model.item.ItemTemplate; import org.l2jmobius.gameserver.model.itemcontainer.Inventory; @@ -104,6 +105,17 @@ public class DropSearchBoard implements IParseBoardHandler private void buildDropIndex() { + NpcData.getInstance().getTemplates(npc -> npc.getDropGroups() != null).forEach(npcTemplate -> + { + for (DropGroupHolder dropGroup : npcTemplate.getDropGroups()) + { + final double chance = dropGroup.getChance() / 100; + for (DropHolder dropHolder : dropGroup.getDropList()) + { + addToDropList(npcTemplate, new DropHolder(dropHolder.getDropType(), dropHolder.getItemId(), dropHolder.getMin(), dropHolder.getMax(), dropHolder.getChance() * chance)); + } + } + }); NpcData.getInstance().getTemplates(npc -> npc.getDropList() != null).forEach(npcTemplate -> { for (DropHolder dropHolder : npcTemplate.getDropList()) diff --git a/L2J_Mobius_Classic_3.0_TheKamael/dist/game/data/xsd/npcs.xsd b/L2J_Mobius_Classic_3.0_TheKamael/dist/game/data/xsd/npcs.xsd index ac78c2d82f..e252403853 100644 --- a/L2J_Mobius_Classic_3.0_TheKamael/dist/game/data/xsd/npcs.xsd +++ b/L2J_Mobius_Classic_3.0_TheKamael/dist/game/data/xsd/npcs.xsd @@ -1,16 +1,5 @@ - - - - - - - - - - - @@ -261,11 +250,81 @@ - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/L2J_Mobius_Classic_3.0_TheKamael/java/org/l2jmobius/gameserver/data/xml/NpcData.java b/L2J_Mobius_Classic_3.0_TheKamael/java/org/l2jmobius/gameserver/data/xml/NpcData.java index a09a0bb79f..838d3899d0 100644 --- a/L2J_Mobius_Classic_3.0_TheKamael/java/org/l2jmobius/gameserver/data/xml/NpcData.java +++ b/L2J_Mobius_Classic_3.0_TheKamael/java/org/l2jmobius/gameserver/data/xml/NpcData.java @@ -47,6 +47,7 @@ import org.l2jmobius.gameserver.enums.MpRewardType; import org.l2jmobius.gameserver.model.StatSet; import org.l2jmobius.gameserver.model.actor.templates.NpcTemplate; import org.l2jmobius.gameserver.model.effects.EffectType; +import org.l2jmobius.gameserver.model.holders.DropGroupHolder; import org.l2jmobius.gameserver.model.holders.DropHolder; import org.l2jmobius.gameserver.model.skill.Skill; @@ -101,6 +102,7 @@ public class NpcData implements IXmlReader Set clans = null; Set ignoreClanNpcIds = null; List dropLists = null; + List dropGroups = null; set.set("id", npcId); set.set("displayId", parseInteger(attrs, "displayId")); set.set("level", parseByte(attrs, "level")); @@ -439,24 +441,54 @@ public class NpcData implements IXmlReader if (dropType != null) { - if (dropLists == null) - { - dropLists = new ArrayList<>(); - } - for (Node dropNode = dropListsNode.getFirstChild(); dropNode != null; dropNode = dropNode.getNextSibling()) { - final NamedNodeMap dropAttrs = dropNode.getAttributes(); - if ("item".equalsIgnoreCase(dropNode.getNodeName())) + final String nodeName = dropNode.getNodeName(); + if (nodeName.equalsIgnoreCase("group")) { - final DropHolder dropItem = new DropHolder(dropType, parseInteger(dropAttrs, "id"), parseLong(dropAttrs, "min"), parseLong(dropAttrs, "max"), parseDouble(dropAttrs, "chance")); - if (ItemTable.getInstance().getTemplate(parseInteger(dropAttrs, "id")) == null) + if (dropGroups == null) { - LOGGER.warning("DropListItem: Could not find item with id " + parseInteger(dropAttrs, "id") + "."); + dropGroups = new ArrayList<>(); + } + + final DropGroupHolder group = new DropGroupHolder(parseDouble(dropNode.getAttributes(), "chance")); + for (Node groupNode = dropNode.getFirstChild(); groupNode != null; groupNode = groupNode.getNextSibling()) + { + if (groupNode.getNodeName().equalsIgnoreCase("item")) + { + final NamedNodeMap groupAttrs = groupNode.getAttributes(); + final int itemId = parseInteger(groupAttrs, "id"); + + if (ItemTable.getInstance().getTemplate(itemId) == null) + { + LOGGER.warning("DropListItem: Could not find item with id " + itemId + "."); + } + else + { + group.addDrop(new DropHolder(dropType, itemId, parseLong(groupAttrs, "min"), parseLong(groupAttrs, "max"), parseDouble(groupAttrs, "chance"))); + } + } + } + + dropGroups.add(group); + } + else if (nodeName.equalsIgnoreCase("item")) + { + if (dropLists == null) + { + dropLists = new ArrayList<>(); + } + + final NamedNodeMap dropAttrs = dropNode.getAttributes(); + final int itemId = parseInteger(dropAttrs, "id"); + + if (ItemTable.getInstance().getTemplate(itemId) == null) + { + LOGGER.warning("DropListItem: Could not find item with id " + itemId + "."); } else { - dropLists.add(dropItem); + dropLists.add(new DropHolder(dropType, itemId, parseLong(dropAttrs, "min"), parseLong(dropAttrs, "max"), parseDouble(dropAttrs, "chance"))); } } } @@ -615,10 +647,21 @@ public class NpcData implements IXmlReader template.setClans(clans); template.setIgnoreClanNpcIds(ignoreClanNpcIds); + // Clean old drop groups. + template.removeDropGroups(); + + // Set new drop groups. + if (dropGroups != null) + { + template.setDropGroups(dropGroups); + } + + // Clean old drop lists. + template.removeDrops(); + + // Set new drop lists. if (dropLists != null) { - template.removeDrops(); - // Drops are sorted by chance (high to low). Collections.sort(dropLists, (d1, d2) -> Double.valueOf(d2.getChance()).compareTo(Double.valueOf(d1.getChance()))); for (DropHolder dropHolder : dropLists) diff --git a/L2J_Mobius_Classic_3.0_TheKamael/java/org/l2jmobius/gameserver/model/actor/templates/NpcTemplate.java b/L2J_Mobius_Classic_3.0_TheKamael/java/org/l2jmobius/gameserver/model/actor/templates/NpcTemplate.java index 94513b66a5..bde4beaba4 100644 --- a/L2J_Mobius_Classic_3.0_TheKamael/java/org/l2jmobius/gameserver/model/actor/templates/NpcTemplate.java +++ b/L2J_Mobius_Classic_3.0_TheKamael/java/org/l2jmobius/gameserver/model/actor/templates/NpcTemplate.java @@ -37,6 +37,7 @@ import org.l2jmobius.gameserver.enums.Race; import org.l2jmobius.gameserver.enums.Sex; import org.l2jmobius.gameserver.model.StatSet; import org.l2jmobius.gameserver.model.actor.Creature; +import org.l2jmobius.gameserver.model.holders.DropGroupHolder; import org.l2jmobius.gameserver.model.holders.DropHolder; import org.l2jmobius.gameserver.model.holders.ItemHolder; import org.l2jmobius.gameserver.model.interfaces.IIdentifiable; @@ -109,6 +110,7 @@ public class NpcTemplate extends CreatureTemplate implements IIdentifiable private Map> _aiSkillLists; private Set _clans; private Set _ignoreClanNpcIds; + private List _dropGroups; private List _dropListDeath; private List _dropListSpoil; private float _collisionRadiusGrown; @@ -665,12 +667,22 @@ public class NpcTemplate extends CreatureTemplate implements IIdentifiable _ignoreClanNpcIds = ignoreClanNpcIds != null ? Collections.unmodifiableSet(ignoreClanNpcIds) : null; } + public void removeDropGroups() + { + _dropGroups = null; + } + public void removeDrops() { _dropListDeath = null; _dropListSpoil = null; } + public void setDropGroups(List groups) + { + _dropGroups = groups; + } + public void addDrop(DropHolder dropHolder) { if (_dropListDeath == null) @@ -689,6 +701,11 @@ public class NpcTemplate extends CreatureTemplate implements IIdentifiable _dropListSpoil.add(dropHolder); } + public List getDropGroups() + { + return _dropGroups; + } + public List getDropList() { return _dropListDeath; @@ -701,11 +718,201 @@ public class NpcTemplate extends CreatureTemplate implements IIdentifiable public List calculateDrops(DropType dropType, Creature victim, Creature killer) { - final List dropList = dropType == DropType.SPOIL ? _dropListSpoil : _dropListDeath; - if (dropList == null) + if (dropType == DropType.DROP) { - return null; + // calculate group drops + List groupDrops = null; + if (_dropGroups != null) + { + groupDrops = calculateGroupDrops(victim, killer); + } + + // calculate ungrouped drops + List ungroupedDrops = null; + if (_dropListDeath != null) + { + ungroupedDrops = calculateUngroupedDrops(dropType, victim, killer); + } + + // return results + if ((groupDrops != null) && (ungroupedDrops != null)) + { + groupDrops.addAll(ungroupedDrops); + ungroupedDrops.clear(); + return groupDrops; + } + if (groupDrops != null) + { + return groupDrops; + } + if (ungroupedDrops != null) + { + return ungroupedDrops; + } } + else if ((dropType == DropType.SPOIL) && (_dropListSpoil != null)) + { + return calculateUngroupedDrops(dropType, victim, killer); + } + + // no drops + return null; + } + + private List calculateGroupDrops(Creature victim, Creature killer) + { + // level difference calculations + final int levelDifference = victim.getLevel() - killer.getLevel(); + final double levelGapChanceToDropAdena = Util.map(levelDifference, -Config.DROP_ADENA_MAX_LEVEL_DIFFERENCE, -Config.DROP_ADENA_MIN_LEVEL_DIFFERENCE, Config.DROP_ADENA_MIN_LEVEL_GAP_CHANCE, 100d); + final double levelGapChanceToDrop = Util.map(levelDifference, -Config.DROP_ITEM_MAX_LEVEL_DIFFERENCE, -Config.DROP_ITEM_MIN_LEVEL_DIFFERENCE, Config.DROP_ITEM_MIN_LEVEL_GAP_CHANCE, 100d); + + int dropOccurrenceCounter = victim.isRaid() ? Config.DROP_MAX_OCCURRENCES_RAIDBOSS : Config.DROP_MAX_OCCURRENCES_NORMAL; + List calculatedDrops = null; + for (DropGroupHolder group : _dropGroups) + { + if (dropOccurrenceCounter <= 0) + { + break; + } + + double groupRate = 1; + final List groupDrops = group.getDropList(); + for (DropHolder dropItem : groupDrops) + { + final int itemId = dropItem.getItemId(); + final ItemTemplate item = ItemTable.getInstance().getTemplate(itemId); + final boolean champion = victim.isChampion(); + + // chance + double rateChance = 1; + final Float itemChance = Config.RATE_DROP_CHANCE_BY_ID.get(itemId); + if (itemChance != null) + { + if (itemChance == 0) + { + continue; + } + + rateChance *= itemChance; + if (champion && (itemId == Inventory.ADENA_ID)) + { + rateChance *= Config.CHAMPION_ADENAS_REWARDS_CHANCE; + } + } + else if (item.hasExImmediateEffect()) + { + rateChance *= Config.RATE_HERB_DROP_CHANCE_MULTIPLIER; + } + else if (victim.isRaid()) + { + rateChance *= Config.RATE_RAID_DROP_CHANCE_MULTIPLIER; + } + else + { + rateChance *= Config.RATE_DEATH_DROP_CHANCE_MULTIPLIER * (champion ? Config.CHAMPION_REWARDS_CHANCE : 1); + } + + // premium chance + if (Config.PREMIUM_SYSTEM_ENABLED && (killer.getActingPlayer() != null) && killer.getActingPlayer().hasPremiumStatus()) + { + if (Config.PREMIUM_RATE_DROP_CHANCE_BY_ID.get(itemId) != null) + { + rateChance *= Config.PREMIUM_RATE_DROP_CHANCE_BY_ID.get(itemId); + } + else if (item.hasExImmediateEffect()) + { + // TODO: Premium herb chance? :) + } + else if (victim.isRaid()) + { + // TODO: Premium raid chance? :) + } + else + { + rateChance *= Config.PREMIUM_RATE_DROP_CHANCE; + } + } + + // keep lowest to avoid chance by id configuration conflicts + groupRate = Math.min(groupRate, rateChance); + } + + if ((Rnd.nextDouble() * 100) < (group.getChance() * groupRate)) + { + double totalChance = 0; // total group chance is 100 + final double dropChance = Rnd.nextDouble() * 100; + GROUP_DROP: for (DropHolder dropItem : groupDrops) + { + if (dropOccurrenceCounter <= 0) + { + break; + } + + // calculate if item will drop + totalChance += dropItem.getChance(); + if (dropChance >= totalChance) + { + continue; + } + + // check level gap that may prevent to drop item + if ((Rnd.nextDouble() * 100) > (dropItem.getItemId() == Inventory.ADENA_ID ? levelGapChanceToDropAdena : levelGapChanceToDrop)) + { + continue; + } + + // create the drop + final ItemHolder drop = calculateGroupDrop(dropItem, victim, killer); + + // create list + if (calculatedDrops == null) + { + calculatedDrops = new ArrayList<>(dropOccurrenceCounter); + } + + // finally + if (group.getChance() < 100) + { + dropOccurrenceCounter--; + } + calculatedDrops.add(drop); + + // no more drops from this group + break GROUP_DROP; + } + } + } + + // champion extra drop + if (victim.isChampion()) + { + if ((victim.getLevel() < killer.getLevel()) && (Rnd.get(100) < Config.CHAMPION_REWARD_LOWER_LEVEL_ITEM_CHANCE)) + { + return calculatedDrops; + } + if ((victim.getLevel() > killer.getLevel()) && (Rnd.get(100) < Config.CHAMPION_REWARD_HIGHER_LEVEL_ITEM_CHANCE)) + { + return calculatedDrops; + } + + // create list + if (calculatedDrops == null) + { + calculatedDrops = new ArrayList<>(); + } + + if (!calculatedDrops.containsAll(Config.CHAMPION_REWARD_ITEMS)) + { + calculatedDrops.addAll(Config.CHAMPION_REWARD_ITEMS); + } + } + + return calculatedDrops; + } + + private List calculateUngroupedDrops(DropType dropType, Creature victim, Creature killer) + { + final List dropList = dropType == DropType.SPOIL ? _dropListSpoil : _dropListDeath; // level difference calculations final int levelDifference = victim.getLevel() - killer.getLevel(); @@ -737,7 +944,7 @@ public class NpcTemplate extends CreatureTemplate implements IIdentifiable } // calculate chances - final ItemHolder drop = calculateDrop(dropItem, victim, killer); + final ItemHolder drop = calculateUngroupedDrop(dropItem, victim, killer); if (drop == null) { continue; @@ -792,7 +999,10 @@ public class NpcTemplate extends CreatureTemplate implements IIdentifiable calculatedDrops = new ArrayList<>(); } - calculatedDrops.addAll(Config.CHAMPION_REWARD_ITEMS); + if (!calculatedDrops.containsAll(Config.CHAMPION_REWARD_ITEMS)) + { + calculatedDrops.addAll(Config.CHAMPION_REWARD_ITEMS); + } } if (Config.VIP_SYSTEM_ENABLED && (dropType == DropType.DROP)) @@ -855,7 +1065,7 @@ public class NpcTemplate extends CreatureTemplate implements IIdentifiable { return null; } - return calculateDrop(dropItem, victim, killer); + return calculateUngroupedDrop(dropItem, victim, killer); } private double calculateLevelGapChanceToDrop(DropHolder dropItem, int levelDifference) @@ -873,13 +1083,72 @@ public class NpcTemplate extends CreatureTemplate implements IIdentifiable } /** - * All item drop chance calculations are done by this method. * @param dropItem * @param victim * @param killer * @return ItemHolder */ - private ItemHolder calculateDrop(DropHolder dropItem, Creature victim, Creature killer) + private ItemHolder calculateGroupDrop(DropHolder dropItem, Creature victim, Creature killer) + { + final int itemId = dropItem.getItemId(); + final ItemTemplate item = ItemTable.getInstance().getTemplate(itemId); + final boolean champion = victim.isChampion(); + + // calculate amount + double rateAmount = 1; + if (Config.RATE_DROP_AMOUNT_BY_ID.get(itemId) != null) + { + rateAmount *= Config.RATE_DROP_AMOUNT_BY_ID.get(itemId); + if (champion && (itemId == Inventory.ADENA_ID)) + { + rateAmount *= Config.CHAMPION_ADENAS_REWARDS_AMOUNT; + } + } + else if (item.hasExImmediateEffect()) + { + rateAmount *= Config.RATE_HERB_DROP_AMOUNT_MULTIPLIER; + } + else if (victim.isRaid()) + { + rateAmount *= Config.RATE_RAID_DROP_AMOUNT_MULTIPLIER; + } + else + { + rateAmount *= Config.RATE_DEATH_DROP_AMOUNT_MULTIPLIER * (champion ? Config.CHAMPION_REWARDS_AMOUNT : 1); + } + + // premium amount + if (Config.PREMIUM_SYSTEM_ENABLED && (killer.getActingPlayer() != null) && killer.getActingPlayer().hasPremiumStatus()) + { + if (Config.PREMIUM_RATE_DROP_AMOUNT_BY_ID.get(itemId) != null) + { + rateAmount *= Config.PREMIUM_RATE_DROP_AMOUNT_BY_ID.get(itemId); + } + else if (item.hasExImmediateEffect()) + { + // TODO: Premium herb amount? :) + } + else if (victim.isRaid()) + { + // TODO: Premium raid amount? :) + } + else + { + rateAmount *= Config.PREMIUM_RATE_DROP_AMOUNT; + } + } + + // finally + return new ItemHolder(itemId, (long) (Rnd.get(dropItem.getMin(), dropItem.getMax()) * rateAmount)); + } + + /** + * @param dropItem + * @param victim + * @param killer + * @return ItemHolder + */ + private ItemHolder calculateUngroupedDrop(DropHolder dropItem, Creature victim, Creature killer) { switch (dropItem.getDropType()) { diff --git a/L2J_Mobius_Classic_3.0_TheKamael/java/org/l2jmobius/gameserver/model/holders/DropGroupHolder.java b/L2J_Mobius_Classic_3.0_TheKamael/java/org/l2jmobius/gameserver/model/holders/DropGroupHolder.java new file mode 100644 index 0000000000..6c2401c0b0 --- /dev/null +++ b/L2J_Mobius_Classic_3.0_TheKamael/java/org/l2jmobius/gameserver/model/holders/DropGroupHolder.java @@ -0,0 +1,49 @@ +/* + * This file is part of the L2J Mobius project. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package org.l2jmobius.gameserver.model.holders; + +import java.util.ArrayList; +import java.util.List; + +/** + * @author Mobius + */ +public class DropGroupHolder +{ + private final List _dropList = new ArrayList<>(); + private final double _chance; + + public DropGroupHolder(double chance) + { + _chance = chance; + } + + public List getDropList() + { + return _dropList; + } + + public void addDrop(DropHolder holder) + { + _dropList.add(holder); + } + + public double getChance() + { + return _chance; + } +} diff --git a/L2J_Mobius_Classic_Interlude/dist/game/data/scripts/handlers/bypasshandlers/NpcViewMod.java b/L2J_Mobius_Classic_Interlude/dist/game/data/scripts/handlers/bypasshandlers/NpcViewMod.java index 35d31315ad..fab79abca8 100644 --- a/L2J_Mobius_Classic_Interlude/dist/game/data/scripts/handlers/bypasshandlers/NpcViewMod.java +++ b/L2J_Mobius_Classic_Interlude/dist/game/data/scripts/handlers/bypasshandlers/NpcViewMod.java @@ -37,6 +37,7 @@ import org.l2jmobius.gameserver.model.actor.Attackable; import org.l2jmobius.gameserver.model.actor.Creature; import org.l2jmobius.gameserver.model.actor.Npc; import org.l2jmobius.gameserver.model.actor.Player; +import org.l2jmobius.gameserver.model.holders.DropGroupHolder; import org.l2jmobius.gameserver.model.holders.DropHolder; import org.l2jmobius.gameserver.model.item.ItemTemplate; import org.l2jmobius.gameserver.model.itemcontainer.Inventory; @@ -326,12 +327,13 @@ public class NpcViewMod implements IBypassHandler private static String getDropListButtons(Npc npc) { final StringBuilder sb = new StringBuilder(); + final List dropListGroups = npc.getTemplate().getDropGroups(); final List dropListDeath = npc.getTemplate().getDropList(); final List dropListSpoil = npc.getTemplate().getSpoilList(); - if ((dropListDeath != null) || (dropListSpoil != null)) + if ((dropListGroups != null) || (dropListDeath != null) || (dropListSpoil != null)) { sb.append("
"); - if (dropListDeath != null) + if ((dropListGroups != null) || (dropListDeath != null)) { sb.append(""); } @@ -348,13 +350,40 @@ public class NpcViewMod implements IBypassHandler private void sendNpcDropList(Player player, Npc npc, DropType dropType, int pageValue) { - final List templateList = dropType == DropType.SPOIL ? npc.getTemplate().getSpoilList() : npc.getTemplate().getDropList(); - if (templateList == null) + List dropList = null; + if (dropType == DropType.SPOIL) + { + dropList = new ArrayList<>(npc.getTemplate().getSpoilList()); + } + else + { + final List drops = npc.getTemplate().getDropList(); + if (drops != null) + { + dropList = new ArrayList<>(drops); + } + final List dropGroups = npc.getTemplate().getDropGroups(); + if (dropGroups != null) + { + if (dropList == null) + { + dropList = new ArrayList<>(); + } + for (DropGroupHolder dropGroup : dropGroups) + { + final double chance = dropGroup.getChance() / 100; + for (DropHolder dropHolder : dropGroup.getDropList()) + { + dropList.add(new DropHolder(dropHolder.getDropType(), dropHolder.getItemId(), dropHolder.getMin(), dropHolder.getMax(), dropHolder.getChance() * chance)); + } + } + } + } + if (dropList == null) { return; } - final List dropList = new ArrayList<>(templateList); Collections.sort(dropList, (d1, d2) -> Integer.valueOf(d1.getItemId()).compareTo(Integer.valueOf(d2.getItemId()))); int pages = dropList.size() / DROP_LIST_ITEMS_PER_PAGE; diff --git a/L2J_Mobius_Classic_Interlude/dist/game/data/scripts/handlers/communityboard/DropSearchBoard.java b/L2J_Mobius_Classic_Interlude/dist/game/data/scripts/handlers/communityboard/DropSearchBoard.java index 647d14cec5..be1d97660b 100644 --- a/L2J_Mobius_Classic_Interlude/dist/game/data/scripts/handlers/communityboard/DropSearchBoard.java +++ b/L2J_Mobius_Classic_Interlude/dist/game/data/scripts/handlers/communityboard/DropSearchBoard.java @@ -36,6 +36,7 @@ import org.l2jmobius.gameserver.handler.CommunityBoardHandler; import org.l2jmobius.gameserver.handler.IParseBoardHandler; import org.l2jmobius.gameserver.model.actor.Player; import org.l2jmobius.gameserver.model.actor.templates.NpcTemplate; +import org.l2jmobius.gameserver.model.holders.DropGroupHolder; import org.l2jmobius.gameserver.model.holders.DropHolder; import org.l2jmobius.gameserver.model.item.ItemTemplate; import org.l2jmobius.gameserver.model.itemcontainer.Inventory; @@ -104,6 +105,17 @@ public class DropSearchBoard implements IParseBoardHandler private void buildDropIndex() { + NpcData.getInstance().getTemplates(npc -> npc.getDropGroups() != null).forEach(npcTemplate -> + { + for (DropGroupHolder dropGroup : npcTemplate.getDropGroups()) + { + final double chance = dropGroup.getChance() / 100; + for (DropHolder dropHolder : dropGroup.getDropList()) + { + addToDropList(npcTemplate, new DropHolder(dropHolder.getDropType(), dropHolder.getItemId(), dropHolder.getMin(), dropHolder.getMax(), dropHolder.getChance() * chance)); + } + } + }); NpcData.getInstance().getTemplates(npc -> npc.getDropList() != null).forEach(npcTemplate -> { for (DropHolder dropHolder : npcTemplate.getDropList()) diff --git a/L2J_Mobius_Classic_Interlude/dist/game/data/xsd/npcs.xsd b/L2J_Mobius_Classic_Interlude/dist/game/data/xsd/npcs.xsd index 117ca4cc1d..2c6e583383 100644 --- a/L2J_Mobius_Classic_Interlude/dist/game/data/xsd/npcs.xsd +++ b/L2J_Mobius_Classic_Interlude/dist/game/data/xsd/npcs.xsd @@ -1,16 +1,5 @@ - - - - - - - - - - - @@ -260,11 +249,81 @@ - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/L2J_Mobius_Classic_Interlude/java/org/l2jmobius/gameserver/data/xml/NpcData.java b/L2J_Mobius_Classic_Interlude/java/org/l2jmobius/gameserver/data/xml/NpcData.java index a80a3e9337..b185bc9196 100644 --- a/L2J_Mobius_Classic_Interlude/java/org/l2jmobius/gameserver/data/xml/NpcData.java +++ b/L2J_Mobius_Classic_Interlude/java/org/l2jmobius/gameserver/data/xml/NpcData.java @@ -46,6 +46,7 @@ import org.l2jmobius.gameserver.enums.MpRewardType; import org.l2jmobius.gameserver.model.StatSet; import org.l2jmobius.gameserver.model.actor.templates.NpcTemplate; import org.l2jmobius.gameserver.model.effects.EffectType; +import org.l2jmobius.gameserver.model.holders.DropGroupHolder; import org.l2jmobius.gameserver.model.holders.DropHolder; import org.l2jmobius.gameserver.model.item.ItemTemplate; import org.l2jmobius.gameserver.model.item.type.CrystalType; @@ -102,6 +103,7 @@ public class NpcData implements IXmlReader Set clans = null; Set ignoreClanNpcIds = null; List dropLists = null; + List dropGroups = null; set.set("id", npcId); set.set("displayId", parseInteger(attrs, "displayId")); set.set("level", parseByte(attrs, "level")); @@ -438,21 +440,59 @@ public class NpcData implements IXmlReader if (dropType != null) { - if (dropLists == null) - { - dropLists = new ArrayList<>(); - } - for (Node dropNode = dropListsNode.getFirstChild(); dropNode != null; dropNode = dropNode.getNextSibling()) { - final NamedNodeMap dropAttrs = dropNode.getAttributes(); - if ("item".equalsIgnoreCase(dropNode.getNodeName())) + final String nodeName = dropNode.getNodeName(); + if (nodeName.equalsIgnoreCase("group")) { - final DropHolder dropItem = new DropHolder(dropType, parseInteger(dropAttrs, "id"), parseLong(dropAttrs, "min"), parseLong(dropAttrs, "max"), parseDouble(dropAttrs, "chance")); - final ItemTemplate item = ItemTable.getInstance().getTemplate(parseInteger(dropAttrs, "id")); + if (dropGroups == null) + { + dropGroups = new ArrayList<>(); + } + + final DropGroupHolder group = new DropGroupHolder(parseDouble(dropNode.getAttributes(), "chance")); + for (Node groupNode = dropNode.getFirstChild(); groupNode != null; groupNode = groupNode.getNextSibling()) + { + if (groupNode.getNodeName().equalsIgnoreCase("item")) + { + final NamedNodeMap groupAttrs = groupNode.getAttributes(); + final int itemId = parseInteger(groupAttrs, "id"); + + final ItemTemplate item = ItemTable.getInstance().getTemplate(itemId); + if (item == null) + { + LOGGER.warning("DropListItem: Could not find item with id " + itemId + "."); + } + else + { + // Max equipable item grade configuration. + final int itemCrystalLevel = item.getCrystalType().getLevel(); + if ((itemCrystalLevel > Config.MAX_EQUIPABLE_ITEM_GRADE.getLevel()) && (itemCrystalLevel < CrystalType.EVENT.getLevel())) + { + continue; + } + + group.addDrop(new DropHolder(dropType, itemId, parseLong(groupAttrs, "min"), parseLong(groupAttrs, "max"), parseDouble(groupAttrs, "chance"))); + } + } + } + + dropGroups.add(group); + } + else if (nodeName.equalsIgnoreCase("item")) + { + if (dropLists == null) + { + dropLists = new ArrayList<>(); + } + + final NamedNodeMap dropAttrs = dropNode.getAttributes(); + final int itemId = parseInteger(dropAttrs, "id"); + + final ItemTemplate item = ItemTable.getInstance().getTemplate(itemId); if (item == null) { - LOGGER.warning("DropListItem: Could not find item with id " + parseInteger(dropAttrs, "id") + "."); + LOGGER.warning("DropListItem: Could not find item with id " + itemId + "."); } else { @@ -463,7 +503,7 @@ public class NpcData implements IXmlReader continue; } - dropLists.add(dropItem); + dropLists.add(new DropHolder(dropType, itemId, parseLong(dropAttrs, "min"), parseLong(dropAttrs, "max"), parseDouble(dropAttrs, "chance"))); } } } @@ -622,10 +662,21 @@ public class NpcData implements IXmlReader template.setClans(clans); template.setIgnoreClanNpcIds(ignoreClanNpcIds); + // Clean old drop groups. + template.removeDropGroups(); + + // Set new drop groups. + if (dropGroups != null) + { + template.setDropGroups(dropGroups); + } + + // Clean old drop lists. + template.removeDrops(); + + // Set new drop lists. if (dropLists != null) { - template.removeDrops(); - // Drops are sorted by chance (high to low). Collections.sort(dropLists, (d1, d2) -> Double.valueOf(d2.getChance()).compareTo(Double.valueOf(d1.getChance()))); for (DropHolder dropHolder : dropLists) diff --git a/L2J_Mobius_Classic_Interlude/java/org/l2jmobius/gameserver/model/actor/templates/NpcTemplate.java b/L2J_Mobius_Classic_Interlude/java/org/l2jmobius/gameserver/model/actor/templates/NpcTemplate.java index 37738b327f..eaef3ee536 100644 --- a/L2J_Mobius_Classic_Interlude/java/org/l2jmobius/gameserver/model/actor/templates/NpcTemplate.java +++ b/L2J_Mobius_Classic_Interlude/java/org/l2jmobius/gameserver/model/actor/templates/NpcTemplate.java @@ -36,6 +36,7 @@ import org.l2jmobius.gameserver.enums.Race; import org.l2jmobius.gameserver.enums.Sex; import org.l2jmobius.gameserver.model.StatSet; import org.l2jmobius.gameserver.model.actor.Creature; +import org.l2jmobius.gameserver.model.holders.DropGroupHolder; import org.l2jmobius.gameserver.model.holders.DropHolder; import org.l2jmobius.gameserver.model.holders.ItemHolder; import org.l2jmobius.gameserver.model.interfaces.IIdentifiable; @@ -107,6 +108,7 @@ public class NpcTemplate extends CreatureTemplate implements IIdentifiable private Map> _aiSkillLists; private Set _clans; private Set _ignoreClanNpcIds; + private List _dropGroups; private List _dropListDeath; private List _dropListSpoil; private float _collisionRadiusGrown; @@ -649,12 +651,22 @@ public class NpcTemplate extends CreatureTemplate implements IIdentifiable _ignoreClanNpcIds = ignoreClanNpcIds != null ? Collections.unmodifiableSet(ignoreClanNpcIds) : null; } + public void removeDropGroups() + { + _dropGroups = null; + } + public void removeDrops() { _dropListDeath = null; _dropListSpoil = null; } + public void setDropGroups(List groups) + { + _dropGroups = groups; + } + public void addDrop(DropHolder dropHolder) { if (_dropListDeath == null) @@ -673,6 +685,11 @@ public class NpcTemplate extends CreatureTemplate implements IIdentifiable _dropListSpoil.add(dropHolder); } + public List getDropGroups() + { + return _dropGroups; + } + public List getDropList() { return _dropListDeath; @@ -685,11 +702,201 @@ public class NpcTemplate extends CreatureTemplate implements IIdentifiable public List calculateDrops(DropType dropType, Creature victim, Creature killer) { - final List dropList = dropType == DropType.SPOIL ? _dropListSpoil : _dropListDeath; - if (dropList == null) + if (dropType == DropType.DROP) { - return null; + // calculate group drops + List groupDrops = null; + if (_dropGroups != null) + { + groupDrops = calculateGroupDrops(victim, killer); + } + + // calculate ungrouped drops + List ungroupedDrops = null; + if (_dropListDeath != null) + { + ungroupedDrops = calculateUngroupedDrops(dropType, victim, killer); + } + + // return results + if ((groupDrops != null) && (ungroupedDrops != null)) + { + groupDrops.addAll(ungroupedDrops); + ungroupedDrops.clear(); + return groupDrops; + } + if (groupDrops != null) + { + return groupDrops; + } + if (ungroupedDrops != null) + { + return ungroupedDrops; + } } + else if ((dropType == DropType.SPOIL) && (_dropListSpoil != null)) + { + return calculateUngroupedDrops(dropType, victim, killer); + } + + // no drops + return null; + } + + private List calculateGroupDrops(Creature victim, Creature killer) + { + // level difference calculations + final int levelDifference = victim.getLevel() - killer.getLevel(); + final double levelGapChanceToDropAdena = Util.map(levelDifference, -Config.DROP_ADENA_MAX_LEVEL_DIFFERENCE, -Config.DROP_ADENA_MIN_LEVEL_DIFFERENCE, Config.DROP_ADENA_MIN_LEVEL_GAP_CHANCE, 100d); + final double levelGapChanceToDrop = Util.map(levelDifference, -Config.DROP_ITEM_MAX_LEVEL_DIFFERENCE, -Config.DROP_ITEM_MIN_LEVEL_DIFFERENCE, Config.DROP_ITEM_MIN_LEVEL_GAP_CHANCE, 100d); + + int dropOccurrenceCounter = victim.isRaid() ? Config.DROP_MAX_OCCURRENCES_RAIDBOSS : Config.DROP_MAX_OCCURRENCES_NORMAL; + List calculatedDrops = null; + for (DropGroupHolder group : _dropGroups) + { + if (dropOccurrenceCounter <= 0) + { + break; + } + + double groupRate = 1; + final List groupDrops = group.getDropList(); + for (DropHolder dropItem : groupDrops) + { + final int itemId = dropItem.getItemId(); + final ItemTemplate item = ItemTable.getInstance().getTemplate(itemId); + final boolean champion = victim.isChampion(); + + // chance + double rateChance = 1; + final Float itemChance = Config.RATE_DROP_CHANCE_BY_ID.get(itemId); + if (itemChance != null) + { + if (itemChance == 0) + { + continue; + } + + rateChance *= itemChance; + if (champion && (itemId == Inventory.ADENA_ID)) + { + rateChance *= Config.CHAMPION_ADENAS_REWARDS_CHANCE; + } + } + else if (item.hasExImmediateEffect()) + { + rateChance *= Config.RATE_HERB_DROP_CHANCE_MULTIPLIER; + } + else if (victim.isRaid()) + { + rateChance *= Config.RATE_RAID_DROP_CHANCE_MULTIPLIER; + } + else + { + rateChance *= Config.RATE_DEATH_DROP_CHANCE_MULTIPLIER * (champion ? Config.CHAMPION_REWARDS_CHANCE : 1); + } + + // premium chance + if (Config.PREMIUM_SYSTEM_ENABLED && (killer.getActingPlayer() != null) && killer.getActingPlayer().hasPremiumStatus()) + { + if (Config.PREMIUM_RATE_DROP_CHANCE_BY_ID.get(itemId) != null) + { + rateChance *= Config.PREMIUM_RATE_DROP_CHANCE_BY_ID.get(itemId); + } + else if (item.hasExImmediateEffect()) + { + // TODO: Premium herb chance? :) + } + else if (victim.isRaid()) + { + // TODO: Premium raid chance? :) + } + else + { + rateChance *= Config.PREMIUM_RATE_DROP_CHANCE; + } + } + + // keep lowest to avoid chance by id configuration conflicts + groupRate = Math.min(groupRate, rateChance); + } + + if ((Rnd.nextDouble() * 100) < (group.getChance() * groupRate)) + { + double totalChance = 0; // total group chance is 100 + final double dropChance = Rnd.nextDouble() * 100; + GROUP_DROP: for (DropHolder dropItem : groupDrops) + { + if (dropOccurrenceCounter <= 0) + { + break; + } + + // calculate if item will drop + totalChance += dropItem.getChance(); + if (dropChance >= totalChance) + { + continue; + } + + // check level gap that may prevent to drop item + if ((Rnd.nextDouble() * 100) > (dropItem.getItemId() == Inventory.ADENA_ID ? levelGapChanceToDropAdena : levelGapChanceToDrop)) + { + continue; + } + + // create the drop + final ItemHolder drop = calculateGroupDrop(dropItem, victim, killer); + + // create list + if (calculatedDrops == null) + { + calculatedDrops = new ArrayList<>(dropOccurrenceCounter); + } + + // finally + if (group.getChance() < 100) + { + dropOccurrenceCounter--; + } + calculatedDrops.add(drop); + + // no more drops from this group + break GROUP_DROP; + } + } + } + + // champion extra drop + if (victim.isChampion()) + { + if ((victim.getLevel() < killer.getLevel()) && (Rnd.get(100) < Config.CHAMPION_REWARD_LOWER_LEVEL_ITEM_CHANCE)) + { + return calculatedDrops; + } + if ((victim.getLevel() > killer.getLevel()) && (Rnd.get(100) < Config.CHAMPION_REWARD_HIGHER_LEVEL_ITEM_CHANCE)) + { + return calculatedDrops; + } + + // create list + if (calculatedDrops == null) + { + calculatedDrops = new ArrayList<>(); + } + + if (!calculatedDrops.containsAll(Config.CHAMPION_REWARD_ITEMS)) + { + calculatedDrops.addAll(Config.CHAMPION_REWARD_ITEMS); + } + } + + return calculatedDrops; + } + + private List calculateUngroupedDrops(DropType dropType, Creature victim, Creature killer) + { + final List dropList = dropType == DropType.SPOIL ? _dropListSpoil : _dropListDeath; // level difference calculations final int levelDifference = victim.getLevel() - killer.getLevel(); @@ -721,7 +928,7 @@ public class NpcTemplate extends CreatureTemplate implements IIdentifiable } // calculate chances - final ItemHolder drop = calculateDrop(dropItem, victim, killer); + final ItemHolder drop = calculateUngroupedDrop(dropItem, victim, killer); if (drop == null) { continue; @@ -776,20 +983,82 @@ public class NpcTemplate extends CreatureTemplate implements IIdentifiable calculatedDrops = new ArrayList<>(); } - calculatedDrops.addAll(Config.CHAMPION_REWARD_ITEMS); + if (!calculatedDrops.containsAll(Config.CHAMPION_REWARD_ITEMS)) + { + calculatedDrops.addAll(Config.CHAMPION_REWARD_ITEMS); + } } return calculatedDrops; } /** - * All item drop chance calculations are done by this method. * @param dropItem * @param victim * @param killer * @return ItemHolder */ - private ItemHolder calculateDrop(DropHolder dropItem, Creature victim, Creature killer) + private ItemHolder calculateGroupDrop(DropHolder dropItem, Creature victim, Creature killer) + { + final int itemId = dropItem.getItemId(); + final ItemTemplate item = ItemTable.getInstance().getTemplate(itemId); + final boolean champion = victim.isChampion(); + + // calculate amount + double rateAmount = 1; + if (Config.RATE_DROP_AMOUNT_BY_ID.get(itemId) != null) + { + rateAmount *= Config.RATE_DROP_AMOUNT_BY_ID.get(itemId); + if (champion && (itemId == Inventory.ADENA_ID)) + { + rateAmount *= Config.CHAMPION_ADENAS_REWARDS_AMOUNT; + } + } + else if (item.hasExImmediateEffect()) + { + rateAmount *= Config.RATE_HERB_DROP_AMOUNT_MULTIPLIER; + } + else if (victim.isRaid()) + { + rateAmount *= Config.RATE_RAID_DROP_AMOUNT_MULTIPLIER; + } + else + { + rateAmount *= Config.RATE_DEATH_DROP_AMOUNT_MULTIPLIER * (champion ? Config.CHAMPION_REWARDS_AMOUNT : 1); + } + + // premium amount + if (Config.PREMIUM_SYSTEM_ENABLED && (killer.getActingPlayer() != null) && killer.getActingPlayer().hasPremiumStatus()) + { + if (Config.PREMIUM_RATE_DROP_AMOUNT_BY_ID.get(itemId) != null) + { + rateAmount *= Config.PREMIUM_RATE_DROP_AMOUNT_BY_ID.get(itemId); + } + else if (item.hasExImmediateEffect()) + { + // TODO: Premium herb amount? :) + } + else if (victim.isRaid()) + { + // TODO: Premium raid amount? :) + } + else + { + rateAmount *= Config.PREMIUM_RATE_DROP_AMOUNT; + } + } + + // finally + return new ItemHolder(itemId, (long) (Rnd.get(dropItem.getMin(), dropItem.getMax()) * rateAmount)); + } + + /** + * @param dropItem + * @param victim + * @param killer + * @return ItemHolder + */ + private ItemHolder calculateUngroupedDrop(DropHolder dropItem, Creature victim, Creature killer) { switch (dropItem.getDropType()) { diff --git a/L2J_Mobius_Classic_Interlude/java/org/l2jmobius/gameserver/model/holders/DropGroupHolder.java b/L2J_Mobius_Classic_Interlude/java/org/l2jmobius/gameserver/model/holders/DropGroupHolder.java new file mode 100644 index 0000000000..6c2401c0b0 --- /dev/null +++ b/L2J_Mobius_Classic_Interlude/java/org/l2jmobius/gameserver/model/holders/DropGroupHolder.java @@ -0,0 +1,49 @@ +/* + * This file is part of the L2J Mobius project. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package org.l2jmobius.gameserver.model.holders; + +import java.util.ArrayList; +import java.util.List; + +/** + * @author Mobius + */ +public class DropGroupHolder +{ + private final List _dropList = new ArrayList<>(); + private final double _chance; + + public DropGroupHolder(double chance) + { + _chance = chance; + } + + public List getDropList() + { + return _dropList; + } + + public void addDrop(DropHolder holder) + { + _dropList.add(holder); + } + + public double getChance() + { + return _chance; + } +} diff --git a/L2J_Mobius_Essence_4.2_DwellingOfSpirits/dist/game/data/scripts/handlers/bypasshandlers/NpcViewMod.java b/L2J_Mobius_Essence_4.2_DwellingOfSpirits/dist/game/data/scripts/handlers/bypasshandlers/NpcViewMod.java index 98919a85ec..8c8b022a2f 100644 --- a/L2J_Mobius_Essence_4.2_DwellingOfSpirits/dist/game/data/scripts/handlers/bypasshandlers/NpcViewMod.java +++ b/L2J_Mobius_Essence_4.2_DwellingOfSpirits/dist/game/data/scripts/handlers/bypasshandlers/NpcViewMod.java @@ -37,6 +37,7 @@ import org.l2jmobius.gameserver.model.actor.Attackable; import org.l2jmobius.gameserver.model.actor.Creature; import org.l2jmobius.gameserver.model.actor.Npc; import org.l2jmobius.gameserver.model.actor.Player; +import org.l2jmobius.gameserver.model.holders.DropGroupHolder; import org.l2jmobius.gameserver.model.holders.DropHolder; import org.l2jmobius.gameserver.model.item.ItemTemplate; import org.l2jmobius.gameserver.model.itemcontainer.Inventory; @@ -326,12 +327,13 @@ public class NpcViewMod implements IBypassHandler private static String getDropListButtons(Npc npc) { final StringBuilder sb = new StringBuilder(); + final List dropListGroups = npc.getTemplate().getDropGroups(); final List dropListDeath = npc.getTemplate().getDropList(); final List dropListSpoil = npc.getTemplate().getSpoilList(); - if ((dropListDeath != null) || (dropListSpoil != null)) + if ((dropListGroups != null) || (dropListDeath != null) || (dropListSpoil != null)) { sb.append("
"); - if (dropListDeath != null) + if ((dropListGroups != null) || (dropListDeath != null)) { sb.append(""); } @@ -348,13 +350,40 @@ public class NpcViewMod implements IBypassHandler private void sendNpcDropList(Player player, Npc npc, DropType dropType, int pageValue) { - final List templateList = dropType == DropType.SPOIL ? npc.getTemplate().getSpoilList() : npc.getTemplate().getDropList(); - if (templateList == null) + List dropList = null; + if (dropType == DropType.SPOIL) + { + dropList = new ArrayList<>(npc.getTemplate().getSpoilList()); + } + else + { + final List drops = npc.getTemplate().getDropList(); + if (drops != null) + { + dropList = new ArrayList<>(drops); + } + final List dropGroups = npc.getTemplate().getDropGroups(); + if (dropGroups != null) + { + if (dropList == null) + { + dropList = new ArrayList<>(); + } + for (DropGroupHolder dropGroup : dropGroups) + { + final double chance = dropGroup.getChance() / 100; + for (DropHolder dropHolder : dropGroup.getDropList()) + { + dropList.add(new DropHolder(dropHolder.getDropType(), dropHolder.getItemId(), dropHolder.getMin(), dropHolder.getMax(), dropHolder.getChance() * chance)); + } + } + } + } + if (dropList == null) { return; } - final List dropList = new ArrayList<>(templateList); Collections.sort(dropList, (d1, d2) -> Integer.valueOf(d1.getItemId()).compareTo(Integer.valueOf(d2.getItemId()))); int pages = dropList.size() / DROP_LIST_ITEMS_PER_PAGE; diff --git a/L2J_Mobius_Essence_4.2_DwellingOfSpirits/dist/game/data/scripts/handlers/communityboard/DropSearchBoard.java b/L2J_Mobius_Essence_4.2_DwellingOfSpirits/dist/game/data/scripts/handlers/communityboard/DropSearchBoard.java index d2a939b605..de79efec37 100644 --- a/L2J_Mobius_Essence_4.2_DwellingOfSpirits/dist/game/data/scripts/handlers/communityboard/DropSearchBoard.java +++ b/L2J_Mobius_Essence_4.2_DwellingOfSpirits/dist/game/data/scripts/handlers/communityboard/DropSearchBoard.java @@ -36,6 +36,7 @@ import org.l2jmobius.gameserver.handler.CommunityBoardHandler; import org.l2jmobius.gameserver.handler.IParseBoardHandler; import org.l2jmobius.gameserver.model.actor.Player; import org.l2jmobius.gameserver.model.actor.templates.NpcTemplate; +import org.l2jmobius.gameserver.model.holders.DropGroupHolder; import org.l2jmobius.gameserver.model.holders.DropHolder; import org.l2jmobius.gameserver.model.item.ItemTemplate; import org.l2jmobius.gameserver.model.itemcontainer.Inventory; @@ -104,6 +105,17 @@ public class DropSearchBoard implements IParseBoardHandler private void buildDropIndex() { + NpcData.getInstance().getTemplates(npc -> npc.getDropGroups() != null).forEach(npcTemplate -> + { + for (DropGroupHolder dropGroup : npcTemplate.getDropGroups()) + { + final double chance = dropGroup.getChance() / 100; + for (DropHolder dropHolder : dropGroup.getDropList()) + { + addToDropList(npcTemplate, new DropHolder(dropHolder.getDropType(), dropHolder.getItemId(), dropHolder.getMin(), dropHolder.getMax(), dropHolder.getChance() * chance)); + } + } + }); NpcData.getInstance().getTemplates(npc -> npc.getDropList() != null).forEach(npcTemplate -> { for (DropHolder dropHolder : npcTemplate.getDropList()) diff --git a/L2J_Mobius_Essence_4.2_DwellingOfSpirits/dist/game/data/xsd/npcs.xsd b/L2J_Mobius_Essence_4.2_DwellingOfSpirits/dist/game/data/xsd/npcs.xsd index ac78c2d82f..9392f96cd7 100644 --- a/L2J_Mobius_Essence_4.2_DwellingOfSpirits/dist/game/data/xsd/npcs.xsd +++ b/L2J_Mobius_Essence_4.2_DwellingOfSpirits/dist/game/data/xsd/npcs.xsd @@ -1,16 +1,5 @@ - - - - - - - - - - - @@ -261,11 +250,81 @@ - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/L2J_Mobius_Essence_4.2_DwellingOfSpirits/java/org/l2jmobius/gameserver/data/xml/NpcData.java b/L2J_Mobius_Essence_4.2_DwellingOfSpirits/java/org/l2jmobius/gameserver/data/xml/NpcData.java index 205b5e56ae..45b18e8cce 100644 --- a/L2J_Mobius_Essence_4.2_DwellingOfSpirits/java/org/l2jmobius/gameserver/data/xml/NpcData.java +++ b/L2J_Mobius_Essence_4.2_DwellingOfSpirits/java/org/l2jmobius/gameserver/data/xml/NpcData.java @@ -47,6 +47,7 @@ import org.l2jmobius.gameserver.enums.MpRewardType; import org.l2jmobius.gameserver.model.StatSet; import org.l2jmobius.gameserver.model.actor.templates.NpcTemplate; import org.l2jmobius.gameserver.model.effects.EffectType; +import org.l2jmobius.gameserver.model.holders.DropGroupHolder; import org.l2jmobius.gameserver.model.holders.DropHolder; import org.l2jmobius.gameserver.model.itemcontainer.Inventory; import org.l2jmobius.gameserver.model.skill.Skill; @@ -104,6 +105,7 @@ public class NpcData implements IXmlReader Set clans = null; Set ignoreClanNpcIds = null; List dropLists = null; + List dropGroups = null; set.set("id", npcId); set.set("displayId", parseInteger(attrs, "displayId")); set.set("level", level); @@ -442,24 +444,66 @@ public class NpcData implements IXmlReader if (dropType != null) { - if (dropLists == null) - { - dropLists = new ArrayList<>(); - } - for (Node dropNode = dropListsNode.getFirstChild(); dropNode != null; dropNode = dropNode.getNextSibling()) { - final NamedNodeMap dropAttrs = dropNode.getAttributes(); - if ("item".equalsIgnoreCase(dropNode.getNodeName())) + final String nodeName = dropNode.getNodeName(); + if (nodeName.equalsIgnoreCase("group")) { - final DropHolder dropItem = new DropHolder(dropType, parseInteger(dropAttrs, "id"), parseLong(dropAttrs, "min"), parseLong(dropAttrs, "max"), parseDouble(dropAttrs, "chance")); - if (ItemTable.getInstance().getTemplate(parseInteger(dropAttrs, "id")) == null) + if (dropGroups == null) { - LOGGER.warning("DropListItem: Could not find item with id " + parseInteger(dropAttrs, "id") + "."); + dropGroups = new ArrayList<>(); + } + + final DropGroupHolder group = new DropGroupHolder(parseDouble(dropNode.getAttributes(), "chance")); + for (Node groupNode = dropNode.getFirstChild(); groupNode != null; groupNode = groupNode.getNextSibling()) + { + if (groupNode.getNodeName().equalsIgnoreCase("item")) + { + final NamedNodeMap groupAttrs = groupNode.getAttributes(); + final int itemId = parseInteger(groupAttrs, "id"); + + // Drop materials for random craft configuration. + if (!Config.DROP_RANDOM_CRAFT_MATERIALS && (itemId >= 92908) && (itemId <= 92919)) + { + continue; + } + + if (ItemTable.getInstance().getTemplate(itemId) == null) + { + LOGGER.warning("DropListItem: Could not find item with id " + itemId + "."); + } + else + { + group.addDrop(new DropHolder(dropType, itemId, parseLong(groupAttrs, "min"), parseLong(groupAttrs, "max"), parseDouble(groupAttrs, "chance"))); + } + } + } + + dropGroups.add(group); + } + else if (nodeName.equalsIgnoreCase("item")) + { + if (dropLists == null) + { + dropLists = new ArrayList<>(); + } + + final NamedNodeMap dropAttrs = dropNode.getAttributes(); + final int itemId = parseInteger(dropAttrs, "id"); + + // Drop materials for random craft configuration. + if (!Config.DROP_RANDOM_CRAFT_MATERIALS && (itemId >= 92908) && (itemId <= 92919)) + { + continue; + } + + if (ItemTable.getInstance().getTemplate(itemId) == null) + { + LOGGER.warning("DropListItem: Could not find item with id " + itemId + "."); } else { - dropLists.add(dropItem); + dropLists.add(new DropHolder(dropType, itemId, parseLong(dropAttrs, "min"), parseLong(dropAttrs, "max"), parseDouble(dropAttrs, "chance"))); } } } @@ -618,32 +662,45 @@ public class NpcData implements IXmlReader template.setClans(clans); template.setIgnoreClanNpcIds(ignoreClanNpcIds); + // Clean old drop groups. + template.removeDropGroups(); + + // Set new drop groups. + if (dropGroups != null) + { + template.setDropGroups(dropGroups); + } + + // Clean old drop lists. + template.removeDrops(); + + // Add LCoin drop for bosses. + if (type.contains("boss")) + { + if (dropLists == null) + { + dropLists = new ArrayList<>(); + } + dropLists.add(new DropHolder(DropType.DROP, Inventory.LCOIN_ID, 1, 1, 100)); + } + + // Add configurable LCoin drop for monsters. + if ((Config.LCOIN_DROP_ENABLED) && (type.contains("Monster") && !type.contains("boss")) && (level >= Config.LCOIN_MIN_MOB_LV)) + { + if (dropLists == null) + { + dropLists = new ArrayList<>(); + } + dropLists.add(new DropHolder(DropType.DROP, Inventory.LCOIN_ID, Config.LCOIN_MIN_QUANTITY, Config.LCOIN_MAX_QUANTITY, Config.LCOIN_DROP_CHANCE)); + } + + // Set new drop lists. if (dropLists != null) { - template.removeDrops(); - - // Add LCoin drop for bosses. - if (type.contains("boss")) - { - dropLists.add(new DropHolder(DropType.DROP, Inventory.LCOIN_ID, 1, 1, 100)); - } - - // Add configurable LCoin drop for monsters. - if ((Config.LCOIN_DROP_ENABLED) && (type.contains("Monster") && !type.contains("boss")) && (level >= Config.LCOIN_MIN_MOB_LV)) - { - dropLists.add(new DropHolder(DropType.DROP, Inventory.LCOIN_ID, Config.LCOIN_MIN_QUANTITY, Config.LCOIN_MAX_QUANTITY, Config.LCOIN_DROP_CHANCE)); - } - // Drops are sorted by chance (high to low). Collections.sort(dropLists, (d1, d2) -> Double.valueOf(d2.getChance()).compareTo(Double.valueOf(d1.getChance()))); for (DropHolder dropHolder : dropLists) { - // Drop materials for random craft configuration. - if (!Config.DROP_RANDOM_CRAFT_MATERIALS && (dropHolder.getItemId() >= 92908) && (dropHolder.getItemId() <= 92919)) - { - continue; - } - switch (dropHolder.getDropType()) { case DROP: diff --git a/L2J_Mobius_Essence_4.2_DwellingOfSpirits/java/org/l2jmobius/gameserver/model/actor/templates/NpcTemplate.java b/L2J_Mobius_Essence_4.2_DwellingOfSpirits/java/org/l2jmobius/gameserver/model/actor/templates/NpcTemplate.java index 173ee61113..ffe745209f 100644 --- a/L2J_Mobius_Essence_4.2_DwellingOfSpirits/java/org/l2jmobius/gameserver/model/actor/templates/NpcTemplate.java +++ b/L2J_Mobius_Essence_4.2_DwellingOfSpirits/java/org/l2jmobius/gameserver/model/actor/templates/NpcTemplate.java @@ -37,6 +37,7 @@ import org.l2jmobius.gameserver.enums.Race; import org.l2jmobius.gameserver.enums.Sex; import org.l2jmobius.gameserver.model.StatSet; import org.l2jmobius.gameserver.model.actor.Creature; +import org.l2jmobius.gameserver.model.holders.DropGroupHolder; import org.l2jmobius.gameserver.model.holders.DropHolder; import org.l2jmobius.gameserver.model.holders.ItemHolder; import org.l2jmobius.gameserver.model.interfaces.IIdentifiable; @@ -108,6 +109,7 @@ public class NpcTemplate extends CreatureTemplate implements IIdentifiable private Map> _aiSkillLists; private Set _clans; private Set _ignoreClanNpcIds; + private List _dropGroups; private List _dropListDeath; private List _dropListSpoil; private float _collisionRadiusGrown; @@ -664,12 +666,22 @@ public class NpcTemplate extends CreatureTemplate implements IIdentifiable _ignoreClanNpcIds = ignoreClanNpcIds != null ? Collections.unmodifiableSet(ignoreClanNpcIds) : null; } + public void removeDropGroups() + { + _dropGroups = null; + } + public void removeDrops() { _dropListDeath = null; _dropListSpoil = null; } + public void setDropGroups(List groups) + { + _dropGroups = groups; + } + public void addDrop(DropHolder dropHolder) { if (_dropListDeath == null) @@ -688,6 +700,11 @@ public class NpcTemplate extends CreatureTemplate implements IIdentifiable _dropListSpoil.add(dropHolder); } + public List getDropGroups() + { + return _dropGroups; + } + public List getDropList() { return _dropListDeath; @@ -700,11 +717,201 @@ public class NpcTemplate extends CreatureTemplate implements IIdentifiable public List calculateDrops(DropType dropType, Creature victim, Creature killer) { - final List dropList = dropType == DropType.SPOIL ? _dropListSpoil : _dropListDeath; - if (dropList == null) + if (dropType == DropType.DROP) { - return null; + // calculate group drops + List groupDrops = null; + if (_dropGroups != null) + { + groupDrops = calculateGroupDrops(victim, killer); + } + + // calculate ungrouped drops + List ungroupedDrops = null; + if (_dropListDeath != null) + { + ungroupedDrops = calculateUngroupedDrops(dropType, victim, killer); + } + + // return results + if ((groupDrops != null) && (ungroupedDrops != null)) + { + groupDrops.addAll(ungroupedDrops); + ungroupedDrops.clear(); + return groupDrops; + } + if (groupDrops != null) + { + return groupDrops; + } + if (ungroupedDrops != null) + { + return ungroupedDrops; + } } + else if ((dropType == DropType.SPOIL) && (_dropListSpoil != null)) + { + return calculateUngroupedDrops(dropType, victim, killer); + } + + // no drops + return null; + } + + private List calculateGroupDrops(Creature victim, Creature killer) + { + // level difference calculations + final int levelDifference = victim.getLevel() - killer.getLevel(); + final double levelGapChanceToDropAdena = Util.map(levelDifference, -Config.DROP_ADENA_MAX_LEVEL_DIFFERENCE, -Config.DROP_ADENA_MIN_LEVEL_DIFFERENCE, Config.DROP_ADENA_MIN_LEVEL_GAP_CHANCE, 100d); + final double levelGapChanceToDrop = Util.map(levelDifference, -Config.DROP_ITEM_MAX_LEVEL_DIFFERENCE, -Config.DROP_ITEM_MIN_LEVEL_DIFFERENCE, Config.DROP_ITEM_MIN_LEVEL_GAP_CHANCE, 100d); + + int dropOccurrenceCounter = victim.isRaid() ? Config.DROP_MAX_OCCURRENCES_RAIDBOSS : Config.DROP_MAX_OCCURRENCES_NORMAL; + List calculatedDrops = null; + for (DropGroupHolder group : _dropGroups) + { + if (dropOccurrenceCounter <= 0) + { + break; + } + + double groupRate = 1; + final List groupDrops = group.getDropList(); + for (DropHolder dropItem : groupDrops) + { + final int itemId = dropItem.getItemId(); + final ItemTemplate item = ItemTable.getInstance().getTemplate(itemId); + final boolean champion = victim.isChampion(); + + // chance + double rateChance = 1; + final Float itemChance = Config.RATE_DROP_CHANCE_BY_ID.get(itemId); + if (itemChance != null) + { + if (itemChance == 0) + { + continue; + } + + rateChance *= itemChance; + if (champion && (itemId == Inventory.ADENA_ID)) + { + rateChance *= Config.CHAMPION_ADENAS_REWARDS_CHANCE; + } + } + else if (item.hasExImmediateEffect()) + { + rateChance *= Config.RATE_HERB_DROP_CHANCE_MULTIPLIER; + } + else if (victim.isRaid()) + { + rateChance *= Config.RATE_RAID_DROP_CHANCE_MULTIPLIER; + } + else + { + rateChance *= Config.RATE_DEATH_DROP_CHANCE_MULTIPLIER * (champion ? Config.CHAMPION_REWARDS_CHANCE : 1); + } + + // premium chance + if (Config.PREMIUM_SYSTEM_ENABLED && (killer.getActingPlayer() != null) && killer.getActingPlayer().hasPremiumStatus()) + { + if (Config.PREMIUM_RATE_DROP_CHANCE_BY_ID.get(itemId) != null) + { + rateChance *= Config.PREMIUM_RATE_DROP_CHANCE_BY_ID.get(itemId); + } + else if (item.hasExImmediateEffect()) + { + // TODO: Premium herb chance? :) + } + else if (victim.isRaid()) + { + // TODO: Premium raid chance? :) + } + else + { + rateChance *= Config.PREMIUM_RATE_DROP_CHANCE; + } + } + + // keep lowest to avoid chance by id configuration conflicts + groupRate = Math.min(groupRate, rateChance); + } + + if ((Rnd.nextDouble() * 100) < (group.getChance() * groupRate)) + { + double totalChance = 0; // total group chance is 100 + final double dropChance = Rnd.nextDouble() * 100; + GROUP_DROP: for (DropHolder dropItem : groupDrops) + { + if (dropOccurrenceCounter <= 0) + { + break; + } + + // calculate if item will drop + totalChance += dropItem.getChance(); + if (dropChance >= totalChance) + { + continue; + } + + // check level gap that may prevent to drop item + if ((Rnd.nextDouble() * 100) > (dropItem.getItemId() == Inventory.ADENA_ID ? levelGapChanceToDropAdena : levelGapChanceToDrop)) + { + continue; + } + + // create the drop + final ItemHolder drop = calculateGroupDrop(dropItem, victim, killer); + + // create list + if (calculatedDrops == null) + { + calculatedDrops = new ArrayList<>(dropOccurrenceCounter); + } + + // finally + if (group.getChance() < 100) + { + dropOccurrenceCounter--; + } + calculatedDrops.add(drop); + + // no more drops from this group + break GROUP_DROP; + } + } + } + + // champion extra drop + if (victim.isChampion()) + { + if ((victim.getLevel() < killer.getLevel()) && (Rnd.get(100) < Config.CHAMPION_REWARD_LOWER_LEVEL_ITEM_CHANCE)) + { + return calculatedDrops; + } + if ((victim.getLevel() > killer.getLevel()) && (Rnd.get(100) < Config.CHAMPION_REWARD_HIGHER_LEVEL_ITEM_CHANCE)) + { + return calculatedDrops; + } + + // create list + if (calculatedDrops == null) + { + calculatedDrops = new ArrayList<>(); + } + + if (!calculatedDrops.containsAll(Config.CHAMPION_REWARD_ITEMS)) + { + calculatedDrops.addAll(Config.CHAMPION_REWARD_ITEMS); + } + } + + return calculatedDrops; + } + + private List calculateUngroupedDrops(DropType dropType, Creature victim, Creature killer) + { + final List dropList = dropType == DropType.SPOIL ? _dropListSpoil : _dropListDeath; // level difference calculations final int levelDifference = victim.getLevel() - killer.getLevel(); @@ -736,7 +943,7 @@ public class NpcTemplate extends CreatureTemplate implements IIdentifiable } // calculate chances - final ItemHolder drop = calculateDrop(dropItem, victim, killer); + final ItemHolder drop = calculateUngroupedDrop(dropItem, victim, killer); if (drop == null) { continue; @@ -791,20 +998,82 @@ public class NpcTemplate extends CreatureTemplate implements IIdentifiable calculatedDrops = new ArrayList<>(); } - calculatedDrops.addAll(Config.CHAMPION_REWARD_ITEMS); + if (!calculatedDrops.containsAll(Config.CHAMPION_REWARD_ITEMS)) + { + calculatedDrops.addAll(Config.CHAMPION_REWARD_ITEMS); + } } return calculatedDrops; } /** - * All item drop chance calculations are done by this method. * @param dropItem * @param victim * @param killer * @return ItemHolder */ - private ItemHolder calculateDrop(DropHolder dropItem, Creature victim, Creature killer) + private ItemHolder calculateGroupDrop(DropHolder dropItem, Creature victim, Creature killer) + { + final int itemId = dropItem.getItemId(); + final ItemTemplate item = ItemTable.getInstance().getTemplate(itemId); + final boolean champion = victim.isChampion(); + + // calculate amount + double rateAmount = 1; + if (Config.RATE_DROP_AMOUNT_BY_ID.get(itemId) != null) + { + rateAmount *= Config.RATE_DROP_AMOUNT_BY_ID.get(itemId); + if (champion && (itemId == Inventory.ADENA_ID)) + { + rateAmount *= Config.CHAMPION_ADENAS_REWARDS_AMOUNT; + } + } + else if (item.hasExImmediateEffect()) + { + rateAmount *= Config.RATE_HERB_DROP_AMOUNT_MULTIPLIER; + } + else if (victim.isRaid()) + { + rateAmount *= Config.RATE_RAID_DROP_AMOUNT_MULTIPLIER; + } + else + { + rateAmount *= Config.RATE_DEATH_DROP_AMOUNT_MULTIPLIER * (champion ? Config.CHAMPION_REWARDS_AMOUNT : 1); + } + + // premium amount + if (Config.PREMIUM_SYSTEM_ENABLED && (killer.getActingPlayer() != null) && killer.getActingPlayer().hasPremiumStatus()) + { + if (Config.PREMIUM_RATE_DROP_AMOUNT_BY_ID.get(itemId) != null) + { + rateAmount *= Config.PREMIUM_RATE_DROP_AMOUNT_BY_ID.get(itemId); + } + else if (item.hasExImmediateEffect()) + { + // TODO: Premium herb amount? :) + } + else if (victim.isRaid()) + { + // TODO: Premium raid amount? :) + } + else + { + rateAmount *= Config.PREMIUM_RATE_DROP_AMOUNT; + } + } + + // finally + return new ItemHolder(itemId, (long) (Rnd.get(dropItem.getMin(), dropItem.getMax()) * rateAmount)); + } + + /** + * @param dropItem + * @param victim + * @param killer + * @return ItemHolder + */ + private ItemHolder calculateUngroupedDrop(DropHolder dropItem, Creature victim, Creature killer) { switch (dropItem.getDropType()) { diff --git a/L2J_Mobius_Essence_4.2_DwellingOfSpirits/java/org/l2jmobius/gameserver/model/holders/DropGroupHolder.java b/L2J_Mobius_Essence_4.2_DwellingOfSpirits/java/org/l2jmobius/gameserver/model/holders/DropGroupHolder.java new file mode 100644 index 0000000000..6c2401c0b0 --- /dev/null +++ b/L2J_Mobius_Essence_4.2_DwellingOfSpirits/java/org/l2jmobius/gameserver/model/holders/DropGroupHolder.java @@ -0,0 +1,49 @@ +/* + * This file is part of the L2J Mobius project. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package org.l2jmobius.gameserver.model.holders; + +import java.util.ArrayList; +import java.util.List; + +/** + * @author Mobius + */ +public class DropGroupHolder +{ + private final List _dropList = new ArrayList<>(); + private final double _chance; + + public DropGroupHolder(double chance) + { + _chance = chance; + } + + public List getDropList() + { + return _dropList; + } + + public void addDrop(DropHolder holder) + { + _dropList.add(holder); + } + + public double getChance() + { + return _chance; + } +} diff --git a/L2J_Mobius_Essence_5.2_FrostLord/dist/game/data/scripts/handlers/bypasshandlers/NpcViewMod.java b/L2J_Mobius_Essence_5.2_FrostLord/dist/game/data/scripts/handlers/bypasshandlers/NpcViewMod.java index 98919a85ec..8c8b022a2f 100644 --- a/L2J_Mobius_Essence_5.2_FrostLord/dist/game/data/scripts/handlers/bypasshandlers/NpcViewMod.java +++ b/L2J_Mobius_Essence_5.2_FrostLord/dist/game/data/scripts/handlers/bypasshandlers/NpcViewMod.java @@ -37,6 +37,7 @@ import org.l2jmobius.gameserver.model.actor.Attackable; import org.l2jmobius.gameserver.model.actor.Creature; import org.l2jmobius.gameserver.model.actor.Npc; import org.l2jmobius.gameserver.model.actor.Player; +import org.l2jmobius.gameserver.model.holders.DropGroupHolder; import org.l2jmobius.gameserver.model.holders.DropHolder; import org.l2jmobius.gameserver.model.item.ItemTemplate; import org.l2jmobius.gameserver.model.itemcontainer.Inventory; @@ -326,12 +327,13 @@ public class NpcViewMod implements IBypassHandler private static String getDropListButtons(Npc npc) { final StringBuilder sb = new StringBuilder(); + final List dropListGroups = npc.getTemplate().getDropGroups(); final List dropListDeath = npc.getTemplate().getDropList(); final List dropListSpoil = npc.getTemplate().getSpoilList(); - if ((dropListDeath != null) || (dropListSpoil != null)) + if ((dropListGroups != null) || (dropListDeath != null) || (dropListSpoil != null)) { sb.append("
"); - if (dropListDeath != null) + if ((dropListGroups != null) || (dropListDeath != null)) { sb.append(""); } @@ -348,13 +350,40 @@ public class NpcViewMod implements IBypassHandler private void sendNpcDropList(Player player, Npc npc, DropType dropType, int pageValue) { - final List templateList = dropType == DropType.SPOIL ? npc.getTemplate().getSpoilList() : npc.getTemplate().getDropList(); - if (templateList == null) + List dropList = null; + if (dropType == DropType.SPOIL) + { + dropList = new ArrayList<>(npc.getTemplate().getSpoilList()); + } + else + { + final List drops = npc.getTemplate().getDropList(); + if (drops != null) + { + dropList = new ArrayList<>(drops); + } + final List dropGroups = npc.getTemplate().getDropGroups(); + if (dropGroups != null) + { + if (dropList == null) + { + dropList = new ArrayList<>(); + } + for (DropGroupHolder dropGroup : dropGroups) + { + final double chance = dropGroup.getChance() / 100; + for (DropHolder dropHolder : dropGroup.getDropList()) + { + dropList.add(new DropHolder(dropHolder.getDropType(), dropHolder.getItemId(), dropHolder.getMin(), dropHolder.getMax(), dropHolder.getChance() * chance)); + } + } + } + } + if (dropList == null) { return; } - final List dropList = new ArrayList<>(templateList); Collections.sort(dropList, (d1, d2) -> Integer.valueOf(d1.getItemId()).compareTo(Integer.valueOf(d2.getItemId()))); int pages = dropList.size() / DROP_LIST_ITEMS_PER_PAGE; diff --git a/L2J_Mobius_Essence_5.2_FrostLord/dist/game/data/scripts/handlers/communityboard/DropSearchBoard.java b/L2J_Mobius_Essence_5.2_FrostLord/dist/game/data/scripts/handlers/communityboard/DropSearchBoard.java index d2a939b605..de79efec37 100644 --- a/L2J_Mobius_Essence_5.2_FrostLord/dist/game/data/scripts/handlers/communityboard/DropSearchBoard.java +++ b/L2J_Mobius_Essence_5.2_FrostLord/dist/game/data/scripts/handlers/communityboard/DropSearchBoard.java @@ -36,6 +36,7 @@ import org.l2jmobius.gameserver.handler.CommunityBoardHandler; import org.l2jmobius.gameserver.handler.IParseBoardHandler; import org.l2jmobius.gameserver.model.actor.Player; import org.l2jmobius.gameserver.model.actor.templates.NpcTemplate; +import org.l2jmobius.gameserver.model.holders.DropGroupHolder; import org.l2jmobius.gameserver.model.holders.DropHolder; import org.l2jmobius.gameserver.model.item.ItemTemplate; import org.l2jmobius.gameserver.model.itemcontainer.Inventory; @@ -104,6 +105,17 @@ public class DropSearchBoard implements IParseBoardHandler private void buildDropIndex() { + NpcData.getInstance().getTemplates(npc -> npc.getDropGroups() != null).forEach(npcTemplate -> + { + for (DropGroupHolder dropGroup : npcTemplate.getDropGroups()) + { + final double chance = dropGroup.getChance() / 100; + for (DropHolder dropHolder : dropGroup.getDropList()) + { + addToDropList(npcTemplate, new DropHolder(dropHolder.getDropType(), dropHolder.getItemId(), dropHolder.getMin(), dropHolder.getMax(), dropHolder.getChance() * chance)); + } + } + }); NpcData.getInstance().getTemplates(npc -> npc.getDropList() != null).forEach(npcTemplate -> { for (DropHolder dropHolder : npcTemplate.getDropList()) diff --git a/L2J_Mobius_Essence_5.2_FrostLord/dist/game/data/xsd/npcs.xsd b/L2J_Mobius_Essence_5.2_FrostLord/dist/game/data/xsd/npcs.xsd index ac78c2d82f..9392f96cd7 100644 --- a/L2J_Mobius_Essence_5.2_FrostLord/dist/game/data/xsd/npcs.xsd +++ b/L2J_Mobius_Essence_5.2_FrostLord/dist/game/data/xsd/npcs.xsd @@ -1,16 +1,5 @@ - - - - - - - - - - - @@ -261,11 +250,81 @@ - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/L2J_Mobius_Essence_5.2_FrostLord/java/org/l2jmobius/gameserver/data/xml/NpcData.java b/L2J_Mobius_Essence_5.2_FrostLord/java/org/l2jmobius/gameserver/data/xml/NpcData.java index 205b5e56ae..45b18e8cce 100644 --- a/L2J_Mobius_Essence_5.2_FrostLord/java/org/l2jmobius/gameserver/data/xml/NpcData.java +++ b/L2J_Mobius_Essence_5.2_FrostLord/java/org/l2jmobius/gameserver/data/xml/NpcData.java @@ -47,6 +47,7 @@ import org.l2jmobius.gameserver.enums.MpRewardType; import org.l2jmobius.gameserver.model.StatSet; import org.l2jmobius.gameserver.model.actor.templates.NpcTemplate; import org.l2jmobius.gameserver.model.effects.EffectType; +import org.l2jmobius.gameserver.model.holders.DropGroupHolder; import org.l2jmobius.gameserver.model.holders.DropHolder; import org.l2jmobius.gameserver.model.itemcontainer.Inventory; import org.l2jmobius.gameserver.model.skill.Skill; @@ -104,6 +105,7 @@ public class NpcData implements IXmlReader Set clans = null; Set ignoreClanNpcIds = null; List dropLists = null; + List dropGroups = null; set.set("id", npcId); set.set("displayId", parseInteger(attrs, "displayId")); set.set("level", level); @@ -442,24 +444,66 @@ public class NpcData implements IXmlReader if (dropType != null) { - if (dropLists == null) - { - dropLists = new ArrayList<>(); - } - for (Node dropNode = dropListsNode.getFirstChild(); dropNode != null; dropNode = dropNode.getNextSibling()) { - final NamedNodeMap dropAttrs = dropNode.getAttributes(); - if ("item".equalsIgnoreCase(dropNode.getNodeName())) + final String nodeName = dropNode.getNodeName(); + if (nodeName.equalsIgnoreCase("group")) { - final DropHolder dropItem = new DropHolder(dropType, parseInteger(dropAttrs, "id"), parseLong(dropAttrs, "min"), parseLong(dropAttrs, "max"), parseDouble(dropAttrs, "chance")); - if (ItemTable.getInstance().getTemplate(parseInteger(dropAttrs, "id")) == null) + if (dropGroups == null) { - LOGGER.warning("DropListItem: Could not find item with id " + parseInteger(dropAttrs, "id") + "."); + dropGroups = new ArrayList<>(); + } + + final DropGroupHolder group = new DropGroupHolder(parseDouble(dropNode.getAttributes(), "chance")); + for (Node groupNode = dropNode.getFirstChild(); groupNode != null; groupNode = groupNode.getNextSibling()) + { + if (groupNode.getNodeName().equalsIgnoreCase("item")) + { + final NamedNodeMap groupAttrs = groupNode.getAttributes(); + final int itemId = parseInteger(groupAttrs, "id"); + + // Drop materials for random craft configuration. + if (!Config.DROP_RANDOM_CRAFT_MATERIALS && (itemId >= 92908) && (itemId <= 92919)) + { + continue; + } + + if (ItemTable.getInstance().getTemplate(itemId) == null) + { + LOGGER.warning("DropListItem: Could not find item with id " + itemId + "."); + } + else + { + group.addDrop(new DropHolder(dropType, itemId, parseLong(groupAttrs, "min"), parseLong(groupAttrs, "max"), parseDouble(groupAttrs, "chance"))); + } + } + } + + dropGroups.add(group); + } + else if (nodeName.equalsIgnoreCase("item")) + { + if (dropLists == null) + { + dropLists = new ArrayList<>(); + } + + final NamedNodeMap dropAttrs = dropNode.getAttributes(); + final int itemId = parseInteger(dropAttrs, "id"); + + // Drop materials for random craft configuration. + if (!Config.DROP_RANDOM_CRAFT_MATERIALS && (itemId >= 92908) && (itemId <= 92919)) + { + continue; + } + + if (ItemTable.getInstance().getTemplate(itemId) == null) + { + LOGGER.warning("DropListItem: Could not find item with id " + itemId + "."); } else { - dropLists.add(dropItem); + dropLists.add(new DropHolder(dropType, itemId, parseLong(dropAttrs, "min"), parseLong(dropAttrs, "max"), parseDouble(dropAttrs, "chance"))); } } } @@ -618,32 +662,45 @@ public class NpcData implements IXmlReader template.setClans(clans); template.setIgnoreClanNpcIds(ignoreClanNpcIds); + // Clean old drop groups. + template.removeDropGroups(); + + // Set new drop groups. + if (dropGroups != null) + { + template.setDropGroups(dropGroups); + } + + // Clean old drop lists. + template.removeDrops(); + + // Add LCoin drop for bosses. + if (type.contains("boss")) + { + if (dropLists == null) + { + dropLists = new ArrayList<>(); + } + dropLists.add(new DropHolder(DropType.DROP, Inventory.LCOIN_ID, 1, 1, 100)); + } + + // Add configurable LCoin drop for monsters. + if ((Config.LCOIN_DROP_ENABLED) && (type.contains("Monster") && !type.contains("boss")) && (level >= Config.LCOIN_MIN_MOB_LV)) + { + if (dropLists == null) + { + dropLists = new ArrayList<>(); + } + dropLists.add(new DropHolder(DropType.DROP, Inventory.LCOIN_ID, Config.LCOIN_MIN_QUANTITY, Config.LCOIN_MAX_QUANTITY, Config.LCOIN_DROP_CHANCE)); + } + + // Set new drop lists. if (dropLists != null) { - template.removeDrops(); - - // Add LCoin drop for bosses. - if (type.contains("boss")) - { - dropLists.add(new DropHolder(DropType.DROP, Inventory.LCOIN_ID, 1, 1, 100)); - } - - // Add configurable LCoin drop for monsters. - if ((Config.LCOIN_DROP_ENABLED) && (type.contains("Monster") && !type.contains("boss")) && (level >= Config.LCOIN_MIN_MOB_LV)) - { - dropLists.add(new DropHolder(DropType.DROP, Inventory.LCOIN_ID, Config.LCOIN_MIN_QUANTITY, Config.LCOIN_MAX_QUANTITY, Config.LCOIN_DROP_CHANCE)); - } - // Drops are sorted by chance (high to low). Collections.sort(dropLists, (d1, d2) -> Double.valueOf(d2.getChance()).compareTo(Double.valueOf(d1.getChance()))); for (DropHolder dropHolder : dropLists) { - // Drop materials for random craft configuration. - if (!Config.DROP_RANDOM_CRAFT_MATERIALS && (dropHolder.getItemId() >= 92908) && (dropHolder.getItemId() <= 92919)) - { - continue; - } - switch (dropHolder.getDropType()) { case DROP: diff --git a/L2J_Mobius_Essence_5.2_FrostLord/java/org/l2jmobius/gameserver/model/actor/templates/NpcTemplate.java b/L2J_Mobius_Essence_5.2_FrostLord/java/org/l2jmobius/gameserver/model/actor/templates/NpcTemplate.java index 173ee61113..ffe745209f 100644 --- a/L2J_Mobius_Essence_5.2_FrostLord/java/org/l2jmobius/gameserver/model/actor/templates/NpcTemplate.java +++ b/L2J_Mobius_Essence_5.2_FrostLord/java/org/l2jmobius/gameserver/model/actor/templates/NpcTemplate.java @@ -37,6 +37,7 @@ import org.l2jmobius.gameserver.enums.Race; import org.l2jmobius.gameserver.enums.Sex; import org.l2jmobius.gameserver.model.StatSet; import org.l2jmobius.gameserver.model.actor.Creature; +import org.l2jmobius.gameserver.model.holders.DropGroupHolder; import org.l2jmobius.gameserver.model.holders.DropHolder; import org.l2jmobius.gameserver.model.holders.ItemHolder; import org.l2jmobius.gameserver.model.interfaces.IIdentifiable; @@ -108,6 +109,7 @@ public class NpcTemplate extends CreatureTemplate implements IIdentifiable private Map> _aiSkillLists; private Set _clans; private Set _ignoreClanNpcIds; + private List _dropGroups; private List _dropListDeath; private List _dropListSpoil; private float _collisionRadiusGrown; @@ -664,12 +666,22 @@ public class NpcTemplate extends CreatureTemplate implements IIdentifiable _ignoreClanNpcIds = ignoreClanNpcIds != null ? Collections.unmodifiableSet(ignoreClanNpcIds) : null; } + public void removeDropGroups() + { + _dropGroups = null; + } + public void removeDrops() { _dropListDeath = null; _dropListSpoil = null; } + public void setDropGroups(List groups) + { + _dropGroups = groups; + } + public void addDrop(DropHolder dropHolder) { if (_dropListDeath == null) @@ -688,6 +700,11 @@ public class NpcTemplate extends CreatureTemplate implements IIdentifiable _dropListSpoil.add(dropHolder); } + public List getDropGroups() + { + return _dropGroups; + } + public List getDropList() { return _dropListDeath; @@ -700,11 +717,201 @@ public class NpcTemplate extends CreatureTemplate implements IIdentifiable public List calculateDrops(DropType dropType, Creature victim, Creature killer) { - final List dropList = dropType == DropType.SPOIL ? _dropListSpoil : _dropListDeath; - if (dropList == null) + if (dropType == DropType.DROP) { - return null; + // calculate group drops + List groupDrops = null; + if (_dropGroups != null) + { + groupDrops = calculateGroupDrops(victim, killer); + } + + // calculate ungrouped drops + List ungroupedDrops = null; + if (_dropListDeath != null) + { + ungroupedDrops = calculateUngroupedDrops(dropType, victim, killer); + } + + // return results + if ((groupDrops != null) && (ungroupedDrops != null)) + { + groupDrops.addAll(ungroupedDrops); + ungroupedDrops.clear(); + return groupDrops; + } + if (groupDrops != null) + { + return groupDrops; + } + if (ungroupedDrops != null) + { + return ungroupedDrops; + } } + else if ((dropType == DropType.SPOIL) && (_dropListSpoil != null)) + { + return calculateUngroupedDrops(dropType, victim, killer); + } + + // no drops + return null; + } + + private List calculateGroupDrops(Creature victim, Creature killer) + { + // level difference calculations + final int levelDifference = victim.getLevel() - killer.getLevel(); + final double levelGapChanceToDropAdena = Util.map(levelDifference, -Config.DROP_ADENA_MAX_LEVEL_DIFFERENCE, -Config.DROP_ADENA_MIN_LEVEL_DIFFERENCE, Config.DROP_ADENA_MIN_LEVEL_GAP_CHANCE, 100d); + final double levelGapChanceToDrop = Util.map(levelDifference, -Config.DROP_ITEM_MAX_LEVEL_DIFFERENCE, -Config.DROP_ITEM_MIN_LEVEL_DIFFERENCE, Config.DROP_ITEM_MIN_LEVEL_GAP_CHANCE, 100d); + + int dropOccurrenceCounter = victim.isRaid() ? Config.DROP_MAX_OCCURRENCES_RAIDBOSS : Config.DROP_MAX_OCCURRENCES_NORMAL; + List calculatedDrops = null; + for (DropGroupHolder group : _dropGroups) + { + if (dropOccurrenceCounter <= 0) + { + break; + } + + double groupRate = 1; + final List groupDrops = group.getDropList(); + for (DropHolder dropItem : groupDrops) + { + final int itemId = dropItem.getItemId(); + final ItemTemplate item = ItemTable.getInstance().getTemplate(itemId); + final boolean champion = victim.isChampion(); + + // chance + double rateChance = 1; + final Float itemChance = Config.RATE_DROP_CHANCE_BY_ID.get(itemId); + if (itemChance != null) + { + if (itemChance == 0) + { + continue; + } + + rateChance *= itemChance; + if (champion && (itemId == Inventory.ADENA_ID)) + { + rateChance *= Config.CHAMPION_ADENAS_REWARDS_CHANCE; + } + } + else if (item.hasExImmediateEffect()) + { + rateChance *= Config.RATE_HERB_DROP_CHANCE_MULTIPLIER; + } + else if (victim.isRaid()) + { + rateChance *= Config.RATE_RAID_DROP_CHANCE_MULTIPLIER; + } + else + { + rateChance *= Config.RATE_DEATH_DROP_CHANCE_MULTIPLIER * (champion ? Config.CHAMPION_REWARDS_CHANCE : 1); + } + + // premium chance + if (Config.PREMIUM_SYSTEM_ENABLED && (killer.getActingPlayer() != null) && killer.getActingPlayer().hasPremiumStatus()) + { + if (Config.PREMIUM_RATE_DROP_CHANCE_BY_ID.get(itemId) != null) + { + rateChance *= Config.PREMIUM_RATE_DROP_CHANCE_BY_ID.get(itemId); + } + else if (item.hasExImmediateEffect()) + { + // TODO: Premium herb chance? :) + } + else if (victim.isRaid()) + { + // TODO: Premium raid chance? :) + } + else + { + rateChance *= Config.PREMIUM_RATE_DROP_CHANCE; + } + } + + // keep lowest to avoid chance by id configuration conflicts + groupRate = Math.min(groupRate, rateChance); + } + + if ((Rnd.nextDouble() * 100) < (group.getChance() * groupRate)) + { + double totalChance = 0; // total group chance is 100 + final double dropChance = Rnd.nextDouble() * 100; + GROUP_DROP: for (DropHolder dropItem : groupDrops) + { + if (dropOccurrenceCounter <= 0) + { + break; + } + + // calculate if item will drop + totalChance += dropItem.getChance(); + if (dropChance >= totalChance) + { + continue; + } + + // check level gap that may prevent to drop item + if ((Rnd.nextDouble() * 100) > (dropItem.getItemId() == Inventory.ADENA_ID ? levelGapChanceToDropAdena : levelGapChanceToDrop)) + { + continue; + } + + // create the drop + final ItemHolder drop = calculateGroupDrop(dropItem, victim, killer); + + // create list + if (calculatedDrops == null) + { + calculatedDrops = new ArrayList<>(dropOccurrenceCounter); + } + + // finally + if (group.getChance() < 100) + { + dropOccurrenceCounter--; + } + calculatedDrops.add(drop); + + // no more drops from this group + break GROUP_DROP; + } + } + } + + // champion extra drop + if (victim.isChampion()) + { + if ((victim.getLevel() < killer.getLevel()) && (Rnd.get(100) < Config.CHAMPION_REWARD_LOWER_LEVEL_ITEM_CHANCE)) + { + return calculatedDrops; + } + if ((victim.getLevel() > killer.getLevel()) && (Rnd.get(100) < Config.CHAMPION_REWARD_HIGHER_LEVEL_ITEM_CHANCE)) + { + return calculatedDrops; + } + + // create list + if (calculatedDrops == null) + { + calculatedDrops = new ArrayList<>(); + } + + if (!calculatedDrops.containsAll(Config.CHAMPION_REWARD_ITEMS)) + { + calculatedDrops.addAll(Config.CHAMPION_REWARD_ITEMS); + } + } + + return calculatedDrops; + } + + private List calculateUngroupedDrops(DropType dropType, Creature victim, Creature killer) + { + final List dropList = dropType == DropType.SPOIL ? _dropListSpoil : _dropListDeath; // level difference calculations final int levelDifference = victim.getLevel() - killer.getLevel(); @@ -736,7 +943,7 @@ public class NpcTemplate extends CreatureTemplate implements IIdentifiable } // calculate chances - final ItemHolder drop = calculateDrop(dropItem, victim, killer); + final ItemHolder drop = calculateUngroupedDrop(dropItem, victim, killer); if (drop == null) { continue; @@ -791,20 +998,82 @@ public class NpcTemplate extends CreatureTemplate implements IIdentifiable calculatedDrops = new ArrayList<>(); } - calculatedDrops.addAll(Config.CHAMPION_REWARD_ITEMS); + if (!calculatedDrops.containsAll(Config.CHAMPION_REWARD_ITEMS)) + { + calculatedDrops.addAll(Config.CHAMPION_REWARD_ITEMS); + } } return calculatedDrops; } /** - * All item drop chance calculations are done by this method. * @param dropItem * @param victim * @param killer * @return ItemHolder */ - private ItemHolder calculateDrop(DropHolder dropItem, Creature victim, Creature killer) + private ItemHolder calculateGroupDrop(DropHolder dropItem, Creature victim, Creature killer) + { + final int itemId = dropItem.getItemId(); + final ItemTemplate item = ItemTable.getInstance().getTemplate(itemId); + final boolean champion = victim.isChampion(); + + // calculate amount + double rateAmount = 1; + if (Config.RATE_DROP_AMOUNT_BY_ID.get(itemId) != null) + { + rateAmount *= Config.RATE_DROP_AMOUNT_BY_ID.get(itemId); + if (champion && (itemId == Inventory.ADENA_ID)) + { + rateAmount *= Config.CHAMPION_ADENAS_REWARDS_AMOUNT; + } + } + else if (item.hasExImmediateEffect()) + { + rateAmount *= Config.RATE_HERB_DROP_AMOUNT_MULTIPLIER; + } + else if (victim.isRaid()) + { + rateAmount *= Config.RATE_RAID_DROP_AMOUNT_MULTIPLIER; + } + else + { + rateAmount *= Config.RATE_DEATH_DROP_AMOUNT_MULTIPLIER * (champion ? Config.CHAMPION_REWARDS_AMOUNT : 1); + } + + // premium amount + if (Config.PREMIUM_SYSTEM_ENABLED && (killer.getActingPlayer() != null) && killer.getActingPlayer().hasPremiumStatus()) + { + if (Config.PREMIUM_RATE_DROP_AMOUNT_BY_ID.get(itemId) != null) + { + rateAmount *= Config.PREMIUM_RATE_DROP_AMOUNT_BY_ID.get(itemId); + } + else if (item.hasExImmediateEffect()) + { + // TODO: Premium herb amount? :) + } + else if (victim.isRaid()) + { + // TODO: Premium raid amount? :) + } + else + { + rateAmount *= Config.PREMIUM_RATE_DROP_AMOUNT; + } + } + + // finally + return new ItemHolder(itemId, (long) (Rnd.get(dropItem.getMin(), dropItem.getMax()) * rateAmount)); + } + + /** + * @param dropItem + * @param victim + * @param killer + * @return ItemHolder + */ + private ItemHolder calculateUngroupedDrop(DropHolder dropItem, Creature victim, Creature killer) { switch (dropItem.getDropType()) { diff --git a/L2J_Mobius_Essence_5.2_FrostLord/java/org/l2jmobius/gameserver/model/holders/DropGroupHolder.java b/L2J_Mobius_Essence_5.2_FrostLord/java/org/l2jmobius/gameserver/model/holders/DropGroupHolder.java new file mode 100644 index 0000000000..6c2401c0b0 --- /dev/null +++ b/L2J_Mobius_Essence_5.2_FrostLord/java/org/l2jmobius/gameserver/model/holders/DropGroupHolder.java @@ -0,0 +1,49 @@ +/* + * This file is part of the L2J Mobius project. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package org.l2jmobius.gameserver.model.holders; + +import java.util.ArrayList; +import java.util.List; + +/** + * @author Mobius + */ +public class DropGroupHolder +{ + private final List _dropList = new ArrayList<>(); + private final double _chance; + + public DropGroupHolder(double chance) + { + _chance = chance; + } + + public List getDropList() + { + return _dropList; + } + + public void addDrop(DropHolder holder) + { + _dropList.add(holder); + } + + public double getChance() + { + return _chance; + } +} diff --git a/L2J_Mobius_Essence_6.1_BattleChronicle/dist/game/data/scripts/handlers/bypasshandlers/NpcViewMod.java b/L2J_Mobius_Essence_6.1_BattleChronicle/dist/game/data/scripts/handlers/bypasshandlers/NpcViewMod.java index 98919a85ec..8c8b022a2f 100644 --- a/L2J_Mobius_Essence_6.1_BattleChronicle/dist/game/data/scripts/handlers/bypasshandlers/NpcViewMod.java +++ b/L2J_Mobius_Essence_6.1_BattleChronicle/dist/game/data/scripts/handlers/bypasshandlers/NpcViewMod.java @@ -37,6 +37,7 @@ import org.l2jmobius.gameserver.model.actor.Attackable; import org.l2jmobius.gameserver.model.actor.Creature; import org.l2jmobius.gameserver.model.actor.Npc; import org.l2jmobius.gameserver.model.actor.Player; +import org.l2jmobius.gameserver.model.holders.DropGroupHolder; import org.l2jmobius.gameserver.model.holders.DropHolder; import org.l2jmobius.gameserver.model.item.ItemTemplate; import org.l2jmobius.gameserver.model.itemcontainer.Inventory; @@ -326,12 +327,13 @@ public class NpcViewMod implements IBypassHandler private static String getDropListButtons(Npc npc) { final StringBuilder sb = new StringBuilder(); + final List dropListGroups = npc.getTemplate().getDropGroups(); final List dropListDeath = npc.getTemplate().getDropList(); final List dropListSpoil = npc.getTemplate().getSpoilList(); - if ((dropListDeath != null) || (dropListSpoil != null)) + if ((dropListGroups != null) || (dropListDeath != null) || (dropListSpoil != null)) { sb.append("
"); - if (dropListDeath != null) + if ((dropListGroups != null) || (dropListDeath != null)) { sb.append(""); } @@ -348,13 +350,40 @@ public class NpcViewMod implements IBypassHandler private void sendNpcDropList(Player player, Npc npc, DropType dropType, int pageValue) { - final List templateList = dropType == DropType.SPOIL ? npc.getTemplate().getSpoilList() : npc.getTemplate().getDropList(); - if (templateList == null) + List dropList = null; + if (dropType == DropType.SPOIL) + { + dropList = new ArrayList<>(npc.getTemplate().getSpoilList()); + } + else + { + final List drops = npc.getTemplate().getDropList(); + if (drops != null) + { + dropList = new ArrayList<>(drops); + } + final List dropGroups = npc.getTemplate().getDropGroups(); + if (dropGroups != null) + { + if (dropList == null) + { + dropList = new ArrayList<>(); + } + for (DropGroupHolder dropGroup : dropGroups) + { + final double chance = dropGroup.getChance() / 100; + for (DropHolder dropHolder : dropGroup.getDropList()) + { + dropList.add(new DropHolder(dropHolder.getDropType(), dropHolder.getItemId(), dropHolder.getMin(), dropHolder.getMax(), dropHolder.getChance() * chance)); + } + } + } + } + if (dropList == null) { return; } - final List dropList = new ArrayList<>(templateList); Collections.sort(dropList, (d1, d2) -> Integer.valueOf(d1.getItemId()).compareTo(Integer.valueOf(d2.getItemId()))); int pages = dropList.size() / DROP_LIST_ITEMS_PER_PAGE; diff --git a/L2J_Mobius_Essence_6.1_BattleChronicle/dist/game/data/scripts/handlers/communityboard/DropSearchBoard.java b/L2J_Mobius_Essence_6.1_BattleChronicle/dist/game/data/scripts/handlers/communityboard/DropSearchBoard.java index d2a939b605..de79efec37 100644 --- a/L2J_Mobius_Essence_6.1_BattleChronicle/dist/game/data/scripts/handlers/communityboard/DropSearchBoard.java +++ b/L2J_Mobius_Essence_6.1_BattleChronicle/dist/game/data/scripts/handlers/communityboard/DropSearchBoard.java @@ -36,6 +36,7 @@ import org.l2jmobius.gameserver.handler.CommunityBoardHandler; import org.l2jmobius.gameserver.handler.IParseBoardHandler; import org.l2jmobius.gameserver.model.actor.Player; import org.l2jmobius.gameserver.model.actor.templates.NpcTemplate; +import org.l2jmobius.gameserver.model.holders.DropGroupHolder; import org.l2jmobius.gameserver.model.holders.DropHolder; import org.l2jmobius.gameserver.model.item.ItemTemplate; import org.l2jmobius.gameserver.model.itemcontainer.Inventory; @@ -104,6 +105,17 @@ public class DropSearchBoard implements IParseBoardHandler private void buildDropIndex() { + NpcData.getInstance().getTemplates(npc -> npc.getDropGroups() != null).forEach(npcTemplate -> + { + for (DropGroupHolder dropGroup : npcTemplate.getDropGroups()) + { + final double chance = dropGroup.getChance() / 100; + for (DropHolder dropHolder : dropGroup.getDropList()) + { + addToDropList(npcTemplate, new DropHolder(dropHolder.getDropType(), dropHolder.getItemId(), dropHolder.getMin(), dropHolder.getMax(), dropHolder.getChance() * chance)); + } + } + }); NpcData.getInstance().getTemplates(npc -> npc.getDropList() != null).forEach(npcTemplate -> { for (DropHolder dropHolder : npcTemplate.getDropList()) diff --git a/L2J_Mobius_Essence_6.1_BattleChronicle/dist/game/data/xsd/npcs.xsd b/L2J_Mobius_Essence_6.1_BattleChronicle/dist/game/data/xsd/npcs.xsd index ac78c2d82f..9392f96cd7 100644 --- a/L2J_Mobius_Essence_6.1_BattleChronicle/dist/game/data/xsd/npcs.xsd +++ b/L2J_Mobius_Essence_6.1_BattleChronicle/dist/game/data/xsd/npcs.xsd @@ -1,16 +1,5 @@ - - - - - - - - - - - @@ -261,11 +250,81 @@ - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/L2J_Mobius_Essence_6.1_BattleChronicle/java/org/l2jmobius/gameserver/data/xml/NpcData.java b/L2J_Mobius_Essence_6.1_BattleChronicle/java/org/l2jmobius/gameserver/data/xml/NpcData.java index 205b5e56ae..45b18e8cce 100644 --- a/L2J_Mobius_Essence_6.1_BattleChronicle/java/org/l2jmobius/gameserver/data/xml/NpcData.java +++ b/L2J_Mobius_Essence_6.1_BattleChronicle/java/org/l2jmobius/gameserver/data/xml/NpcData.java @@ -47,6 +47,7 @@ import org.l2jmobius.gameserver.enums.MpRewardType; import org.l2jmobius.gameserver.model.StatSet; import org.l2jmobius.gameserver.model.actor.templates.NpcTemplate; import org.l2jmobius.gameserver.model.effects.EffectType; +import org.l2jmobius.gameserver.model.holders.DropGroupHolder; import org.l2jmobius.gameserver.model.holders.DropHolder; import org.l2jmobius.gameserver.model.itemcontainer.Inventory; import org.l2jmobius.gameserver.model.skill.Skill; @@ -104,6 +105,7 @@ public class NpcData implements IXmlReader Set clans = null; Set ignoreClanNpcIds = null; List dropLists = null; + List dropGroups = null; set.set("id", npcId); set.set("displayId", parseInteger(attrs, "displayId")); set.set("level", level); @@ -442,24 +444,66 @@ public class NpcData implements IXmlReader if (dropType != null) { - if (dropLists == null) - { - dropLists = new ArrayList<>(); - } - for (Node dropNode = dropListsNode.getFirstChild(); dropNode != null; dropNode = dropNode.getNextSibling()) { - final NamedNodeMap dropAttrs = dropNode.getAttributes(); - if ("item".equalsIgnoreCase(dropNode.getNodeName())) + final String nodeName = dropNode.getNodeName(); + if (nodeName.equalsIgnoreCase("group")) { - final DropHolder dropItem = new DropHolder(dropType, parseInteger(dropAttrs, "id"), parseLong(dropAttrs, "min"), parseLong(dropAttrs, "max"), parseDouble(dropAttrs, "chance")); - if (ItemTable.getInstance().getTemplate(parseInteger(dropAttrs, "id")) == null) + if (dropGroups == null) { - LOGGER.warning("DropListItem: Could not find item with id " + parseInteger(dropAttrs, "id") + "."); + dropGroups = new ArrayList<>(); + } + + final DropGroupHolder group = new DropGroupHolder(parseDouble(dropNode.getAttributes(), "chance")); + for (Node groupNode = dropNode.getFirstChild(); groupNode != null; groupNode = groupNode.getNextSibling()) + { + if (groupNode.getNodeName().equalsIgnoreCase("item")) + { + final NamedNodeMap groupAttrs = groupNode.getAttributes(); + final int itemId = parseInteger(groupAttrs, "id"); + + // Drop materials for random craft configuration. + if (!Config.DROP_RANDOM_CRAFT_MATERIALS && (itemId >= 92908) && (itemId <= 92919)) + { + continue; + } + + if (ItemTable.getInstance().getTemplate(itemId) == null) + { + LOGGER.warning("DropListItem: Could not find item with id " + itemId + "."); + } + else + { + group.addDrop(new DropHolder(dropType, itemId, parseLong(groupAttrs, "min"), parseLong(groupAttrs, "max"), parseDouble(groupAttrs, "chance"))); + } + } + } + + dropGroups.add(group); + } + else if (nodeName.equalsIgnoreCase("item")) + { + if (dropLists == null) + { + dropLists = new ArrayList<>(); + } + + final NamedNodeMap dropAttrs = dropNode.getAttributes(); + final int itemId = parseInteger(dropAttrs, "id"); + + // Drop materials for random craft configuration. + if (!Config.DROP_RANDOM_CRAFT_MATERIALS && (itemId >= 92908) && (itemId <= 92919)) + { + continue; + } + + if (ItemTable.getInstance().getTemplate(itemId) == null) + { + LOGGER.warning("DropListItem: Could not find item with id " + itemId + "."); } else { - dropLists.add(dropItem); + dropLists.add(new DropHolder(dropType, itemId, parseLong(dropAttrs, "min"), parseLong(dropAttrs, "max"), parseDouble(dropAttrs, "chance"))); } } } @@ -618,32 +662,45 @@ public class NpcData implements IXmlReader template.setClans(clans); template.setIgnoreClanNpcIds(ignoreClanNpcIds); + // Clean old drop groups. + template.removeDropGroups(); + + // Set new drop groups. + if (dropGroups != null) + { + template.setDropGroups(dropGroups); + } + + // Clean old drop lists. + template.removeDrops(); + + // Add LCoin drop for bosses. + if (type.contains("boss")) + { + if (dropLists == null) + { + dropLists = new ArrayList<>(); + } + dropLists.add(new DropHolder(DropType.DROP, Inventory.LCOIN_ID, 1, 1, 100)); + } + + // Add configurable LCoin drop for monsters. + if ((Config.LCOIN_DROP_ENABLED) && (type.contains("Monster") && !type.contains("boss")) && (level >= Config.LCOIN_MIN_MOB_LV)) + { + if (dropLists == null) + { + dropLists = new ArrayList<>(); + } + dropLists.add(new DropHolder(DropType.DROP, Inventory.LCOIN_ID, Config.LCOIN_MIN_QUANTITY, Config.LCOIN_MAX_QUANTITY, Config.LCOIN_DROP_CHANCE)); + } + + // Set new drop lists. if (dropLists != null) { - template.removeDrops(); - - // Add LCoin drop for bosses. - if (type.contains("boss")) - { - dropLists.add(new DropHolder(DropType.DROP, Inventory.LCOIN_ID, 1, 1, 100)); - } - - // Add configurable LCoin drop for monsters. - if ((Config.LCOIN_DROP_ENABLED) && (type.contains("Monster") && !type.contains("boss")) && (level >= Config.LCOIN_MIN_MOB_LV)) - { - dropLists.add(new DropHolder(DropType.DROP, Inventory.LCOIN_ID, Config.LCOIN_MIN_QUANTITY, Config.LCOIN_MAX_QUANTITY, Config.LCOIN_DROP_CHANCE)); - } - // Drops are sorted by chance (high to low). Collections.sort(dropLists, (d1, d2) -> Double.valueOf(d2.getChance()).compareTo(Double.valueOf(d1.getChance()))); for (DropHolder dropHolder : dropLists) { - // Drop materials for random craft configuration. - if (!Config.DROP_RANDOM_CRAFT_MATERIALS && (dropHolder.getItemId() >= 92908) && (dropHolder.getItemId() <= 92919)) - { - continue; - } - switch (dropHolder.getDropType()) { case DROP: diff --git a/L2J_Mobius_Essence_6.1_BattleChronicle/java/org/l2jmobius/gameserver/model/actor/templates/NpcTemplate.java b/L2J_Mobius_Essence_6.1_BattleChronicle/java/org/l2jmobius/gameserver/model/actor/templates/NpcTemplate.java index 173ee61113..ffe745209f 100644 --- a/L2J_Mobius_Essence_6.1_BattleChronicle/java/org/l2jmobius/gameserver/model/actor/templates/NpcTemplate.java +++ b/L2J_Mobius_Essence_6.1_BattleChronicle/java/org/l2jmobius/gameserver/model/actor/templates/NpcTemplate.java @@ -37,6 +37,7 @@ import org.l2jmobius.gameserver.enums.Race; import org.l2jmobius.gameserver.enums.Sex; import org.l2jmobius.gameserver.model.StatSet; import org.l2jmobius.gameserver.model.actor.Creature; +import org.l2jmobius.gameserver.model.holders.DropGroupHolder; import org.l2jmobius.gameserver.model.holders.DropHolder; import org.l2jmobius.gameserver.model.holders.ItemHolder; import org.l2jmobius.gameserver.model.interfaces.IIdentifiable; @@ -108,6 +109,7 @@ public class NpcTemplate extends CreatureTemplate implements IIdentifiable private Map> _aiSkillLists; private Set _clans; private Set _ignoreClanNpcIds; + private List _dropGroups; private List _dropListDeath; private List _dropListSpoil; private float _collisionRadiusGrown; @@ -664,12 +666,22 @@ public class NpcTemplate extends CreatureTemplate implements IIdentifiable _ignoreClanNpcIds = ignoreClanNpcIds != null ? Collections.unmodifiableSet(ignoreClanNpcIds) : null; } + public void removeDropGroups() + { + _dropGroups = null; + } + public void removeDrops() { _dropListDeath = null; _dropListSpoil = null; } + public void setDropGroups(List groups) + { + _dropGroups = groups; + } + public void addDrop(DropHolder dropHolder) { if (_dropListDeath == null) @@ -688,6 +700,11 @@ public class NpcTemplate extends CreatureTemplate implements IIdentifiable _dropListSpoil.add(dropHolder); } + public List getDropGroups() + { + return _dropGroups; + } + public List getDropList() { return _dropListDeath; @@ -700,11 +717,201 @@ public class NpcTemplate extends CreatureTemplate implements IIdentifiable public List calculateDrops(DropType dropType, Creature victim, Creature killer) { - final List dropList = dropType == DropType.SPOIL ? _dropListSpoil : _dropListDeath; - if (dropList == null) + if (dropType == DropType.DROP) { - return null; + // calculate group drops + List groupDrops = null; + if (_dropGroups != null) + { + groupDrops = calculateGroupDrops(victim, killer); + } + + // calculate ungrouped drops + List ungroupedDrops = null; + if (_dropListDeath != null) + { + ungroupedDrops = calculateUngroupedDrops(dropType, victim, killer); + } + + // return results + if ((groupDrops != null) && (ungroupedDrops != null)) + { + groupDrops.addAll(ungroupedDrops); + ungroupedDrops.clear(); + return groupDrops; + } + if (groupDrops != null) + { + return groupDrops; + } + if (ungroupedDrops != null) + { + return ungroupedDrops; + } } + else if ((dropType == DropType.SPOIL) && (_dropListSpoil != null)) + { + return calculateUngroupedDrops(dropType, victim, killer); + } + + // no drops + return null; + } + + private List calculateGroupDrops(Creature victim, Creature killer) + { + // level difference calculations + final int levelDifference = victim.getLevel() - killer.getLevel(); + final double levelGapChanceToDropAdena = Util.map(levelDifference, -Config.DROP_ADENA_MAX_LEVEL_DIFFERENCE, -Config.DROP_ADENA_MIN_LEVEL_DIFFERENCE, Config.DROP_ADENA_MIN_LEVEL_GAP_CHANCE, 100d); + final double levelGapChanceToDrop = Util.map(levelDifference, -Config.DROP_ITEM_MAX_LEVEL_DIFFERENCE, -Config.DROP_ITEM_MIN_LEVEL_DIFFERENCE, Config.DROP_ITEM_MIN_LEVEL_GAP_CHANCE, 100d); + + int dropOccurrenceCounter = victim.isRaid() ? Config.DROP_MAX_OCCURRENCES_RAIDBOSS : Config.DROP_MAX_OCCURRENCES_NORMAL; + List calculatedDrops = null; + for (DropGroupHolder group : _dropGroups) + { + if (dropOccurrenceCounter <= 0) + { + break; + } + + double groupRate = 1; + final List groupDrops = group.getDropList(); + for (DropHolder dropItem : groupDrops) + { + final int itemId = dropItem.getItemId(); + final ItemTemplate item = ItemTable.getInstance().getTemplate(itemId); + final boolean champion = victim.isChampion(); + + // chance + double rateChance = 1; + final Float itemChance = Config.RATE_DROP_CHANCE_BY_ID.get(itemId); + if (itemChance != null) + { + if (itemChance == 0) + { + continue; + } + + rateChance *= itemChance; + if (champion && (itemId == Inventory.ADENA_ID)) + { + rateChance *= Config.CHAMPION_ADENAS_REWARDS_CHANCE; + } + } + else if (item.hasExImmediateEffect()) + { + rateChance *= Config.RATE_HERB_DROP_CHANCE_MULTIPLIER; + } + else if (victim.isRaid()) + { + rateChance *= Config.RATE_RAID_DROP_CHANCE_MULTIPLIER; + } + else + { + rateChance *= Config.RATE_DEATH_DROP_CHANCE_MULTIPLIER * (champion ? Config.CHAMPION_REWARDS_CHANCE : 1); + } + + // premium chance + if (Config.PREMIUM_SYSTEM_ENABLED && (killer.getActingPlayer() != null) && killer.getActingPlayer().hasPremiumStatus()) + { + if (Config.PREMIUM_RATE_DROP_CHANCE_BY_ID.get(itemId) != null) + { + rateChance *= Config.PREMIUM_RATE_DROP_CHANCE_BY_ID.get(itemId); + } + else if (item.hasExImmediateEffect()) + { + // TODO: Premium herb chance? :) + } + else if (victim.isRaid()) + { + // TODO: Premium raid chance? :) + } + else + { + rateChance *= Config.PREMIUM_RATE_DROP_CHANCE; + } + } + + // keep lowest to avoid chance by id configuration conflicts + groupRate = Math.min(groupRate, rateChance); + } + + if ((Rnd.nextDouble() * 100) < (group.getChance() * groupRate)) + { + double totalChance = 0; // total group chance is 100 + final double dropChance = Rnd.nextDouble() * 100; + GROUP_DROP: for (DropHolder dropItem : groupDrops) + { + if (dropOccurrenceCounter <= 0) + { + break; + } + + // calculate if item will drop + totalChance += dropItem.getChance(); + if (dropChance >= totalChance) + { + continue; + } + + // check level gap that may prevent to drop item + if ((Rnd.nextDouble() * 100) > (dropItem.getItemId() == Inventory.ADENA_ID ? levelGapChanceToDropAdena : levelGapChanceToDrop)) + { + continue; + } + + // create the drop + final ItemHolder drop = calculateGroupDrop(dropItem, victim, killer); + + // create list + if (calculatedDrops == null) + { + calculatedDrops = new ArrayList<>(dropOccurrenceCounter); + } + + // finally + if (group.getChance() < 100) + { + dropOccurrenceCounter--; + } + calculatedDrops.add(drop); + + // no more drops from this group + break GROUP_DROP; + } + } + } + + // champion extra drop + if (victim.isChampion()) + { + if ((victim.getLevel() < killer.getLevel()) && (Rnd.get(100) < Config.CHAMPION_REWARD_LOWER_LEVEL_ITEM_CHANCE)) + { + return calculatedDrops; + } + if ((victim.getLevel() > killer.getLevel()) && (Rnd.get(100) < Config.CHAMPION_REWARD_HIGHER_LEVEL_ITEM_CHANCE)) + { + return calculatedDrops; + } + + // create list + if (calculatedDrops == null) + { + calculatedDrops = new ArrayList<>(); + } + + if (!calculatedDrops.containsAll(Config.CHAMPION_REWARD_ITEMS)) + { + calculatedDrops.addAll(Config.CHAMPION_REWARD_ITEMS); + } + } + + return calculatedDrops; + } + + private List calculateUngroupedDrops(DropType dropType, Creature victim, Creature killer) + { + final List dropList = dropType == DropType.SPOIL ? _dropListSpoil : _dropListDeath; // level difference calculations final int levelDifference = victim.getLevel() - killer.getLevel(); @@ -736,7 +943,7 @@ public class NpcTemplate extends CreatureTemplate implements IIdentifiable } // calculate chances - final ItemHolder drop = calculateDrop(dropItem, victim, killer); + final ItemHolder drop = calculateUngroupedDrop(dropItem, victim, killer); if (drop == null) { continue; @@ -791,20 +998,82 @@ public class NpcTemplate extends CreatureTemplate implements IIdentifiable calculatedDrops = new ArrayList<>(); } - calculatedDrops.addAll(Config.CHAMPION_REWARD_ITEMS); + if (!calculatedDrops.containsAll(Config.CHAMPION_REWARD_ITEMS)) + { + calculatedDrops.addAll(Config.CHAMPION_REWARD_ITEMS); + } } return calculatedDrops; } /** - * All item drop chance calculations are done by this method. * @param dropItem * @param victim * @param killer * @return ItemHolder */ - private ItemHolder calculateDrop(DropHolder dropItem, Creature victim, Creature killer) + private ItemHolder calculateGroupDrop(DropHolder dropItem, Creature victim, Creature killer) + { + final int itemId = dropItem.getItemId(); + final ItemTemplate item = ItemTable.getInstance().getTemplate(itemId); + final boolean champion = victim.isChampion(); + + // calculate amount + double rateAmount = 1; + if (Config.RATE_DROP_AMOUNT_BY_ID.get(itemId) != null) + { + rateAmount *= Config.RATE_DROP_AMOUNT_BY_ID.get(itemId); + if (champion && (itemId == Inventory.ADENA_ID)) + { + rateAmount *= Config.CHAMPION_ADENAS_REWARDS_AMOUNT; + } + } + else if (item.hasExImmediateEffect()) + { + rateAmount *= Config.RATE_HERB_DROP_AMOUNT_MULTIPLIER; + } + else if (victim.isRaid()) + { + rateAmount *= Config.RATE_RAID_DROP_AMOUNT_MULTIPLIER; + } + else + { + rateAmount *= Config.RATE_DEATH_DROP_AMOUNT_MULTIPLIER * (champion ? Config.CHAMPION_REWARDS_AMOUNT : 1); + } + + // premium amount + if (Config.PREMIUM_SYSTEM_ENABLED && (killer.getActingPlayer() != null) && killer.getActingPlayer().hasPremiumStatus()) + { + if (Config.PREMIUM_RATE_DROP_AMOUNT_BY_ID.get(itemId) != null) + { + rateAmount *= Config.PREMIUM_RATE_DROP_AMOUNT_BY_ID.get(itemId); + } + else if (item.hasExImmediateEffect()) + { + // TODO: Premium herb amount? :) + } + else if (victim.isRaid()) + { + // TODO: Premium raid amount? :) + } + else + { + rateAmount *= Config.PREMIUM_RATE_DROP_AMOUNT; + } + } + + // finally + return new ItemHolder(itemId, (long) (Rnd.get(dropItem.getMin(), dropItem.getMax()) * rateAmount)); + } + + /** + * @param dropItem + * @param victim + * @param killer + * @return ItemHolder + */ + private ItemHolder calculateUngroupedDrop(DropHolder dropItem, Creature victim, Creature killer) { switch (dropItem.getDropType()) { diff --git a/L2J_Mobius_Essence_6.1_BattleChronicle/java/org/l2jmobius/gameserver/model/holders/DropGroupHolder.java b/L2J_Mobius_Essence_6.1_BattleChronicle/java/org/l2jmobius/gameserver/model/holders/DropGroupHolder.java new file mode 100644 index 0000000000..6c2401c0b0 --- /dev/null +++ b/L2J_Mobius_Essence_6.1_BattleChronicle/java/org/l2jmobius/gameserver/model/holders/DropGroupHolder.java @@ -0,0 +1,49 @@ +/* + * This file is part of the L2J Mobius project. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package org.l2jmobius.gameserver.model.holders; + +import java.util.ArrayList; +import java.util.List; + +/** + * @author Mobius + */ +public class DropGroupHolder +{ + private final List _dropList = new ArrayList<>(); + private final double _chance; + + public DropGroupHolder(double chance) + { + _chance = chance; + } + + public List getDropList() + { + return _dropList; + } + + public void addDrop(DropHolder holder) + { + _dropList.add(holder); + } + + public double getChance() + { + return _chance; + } +} diff --git a/L2J_Mobius_Essence_6.2_Vanguard/dist/game/data/scripts/handlers/bypasshandlers/NpcViewMod.java b/L2J_Mobius_Essence_6.2_Vanguard/dist/game/data/scripts/handlers/bypasshandlers/NpcViewMod.java index 98919a85ec..8c8b022a2f 100644 --- a/L2J_Mobius_Essence_6.2_Vanguard/dist/game/data/scripts/handlers/bypasshandlers/NpcViewMod.java +++ b/L2J_Mobius_Essence_6.2_Vanguard/dist/game/data/scripts/handlers/bypasshandlers/NpcViewMod.java @@ -37,6 +37,7 @@ import org.l2jmobius.gameserver.model.actor.Attackable; import org.l2jmobius.gameserver.model.actor.Creature; import org.l2jmobius.gameserver.model.actor.Npc; import org.l2jmobius.gameserver.model.actor.Player; +import org.l2jmobius.gameserver.model.holders.DropGroupHolder; import org.l2jmobius.gameserver.model.holders.DropHolder; import org.l2jmobius.gameserver.model.item.ItemTemplate; import org.l2jmobius.gameserver.model.itemcontainer.Inventory; @@ -326,12 +327,13 @@ public class NpcViewMod implements IBypassHandler private static String getDropListButtons(Npc npc) { final StringBuilder sb = new StringBuilder(); + final List dropListGroups = npc.getTemplate().getDropGroups(); final List dropListDeath = npc.getTemplate().getDropList(); final List dropListSpoil = npc.getTemplate().getSpoilList(); - if ((dropListDeath != null) || (dropListSpoil != null)) + if ((dropListGroups != null) || (dropListDeath != null) || (dropListSpoil != null)) { sb.append("
"); - if (dropListDeath != null) + if ((dropListGroups != null) || (dropListDeath != null)) { sb.append(""); } @@ -348,13 +350,40 @@ public class NpcViewMod implements IBypassHandler private void sendNpcDropList(Player player, Npc npc, DropType dropType, int pageValue) { - final List templateList = dropType == DropType.SPOIL ? npc.getTemplate().getSpoilList() : npc.getTemplate().getDropList(); - if (templateList == null) + List dropList = null; + if (dropType == DropType.SPOIL) + { + dropList = new ArrayList<>(npc.getTemplate().getSpoilList()); + } + else + { + final List drops = npc.getTemplate().getDropList(); + if (drops != null) + { + dropList = new ArrayList<>(drops); + } + final List dropGroups = npc.getTemplate().getDropGroups(); + if (dropGroups != null) + { + if (dropList == null) + { + dropList = new ArrayList<>(); + } + for (DropGroupHolder dropGroup : dropGroups) + { + final double chance = dropGroup.getChance() / 100; + for (DropHolder dropHolder : dropGroup.getDropList()) + { + dropList.add(new DropHolder(dropHolder.getDropType(), dropHolder.getItemId(), dropHolder.getMin(), dropHolder.getMax(), dropHolder.getChance() * chance)); + } + } + } + } + if (dropList == null) { return; } - final List dropList = new ArrayList<>(templateList); Collections.sort(dropList, (d1, d2) -> Integer.valueOf(d1.getItemId()).compareTo(Integer.valueOf(d2.getItemId()))); int pages = dropList.size() / DROP_LIST_ITEMS_PER_PAGE; diff --git a/L2J_Mobius_Essence_6.2_Vanguard/dist/game/data/scripts/handlers/communityboard/DropSearchBoard.java b/L2J_Mobius_Essence_6.2_Vanguard/dist/game/data/scripts/handlers/communityboard/DropSearchBoard.java index d2a939b605..de79efec37 100644 --- a/L2J_Mobius_Essence_6.2_Vanguard/dist/game/data/scripts/handlers/communityboard/DropSearchBoard.java +++ b/L2J_Mobius_Essence_6.2_Vanguard/dist/game/data/scripts/handlers/communityboard/DropSearchBoard.java @@ -36,6 +36,7 @@ import org.l2jmobius.gameserver.handler.CommunityBoardHandler; import org.l2jmobius.gameserver.handler.IParseBoardHandler; import org.l2jmobius.gameserver.model.actor.Player; import org.l2jmobius.gameserver.model.actor.templates.NpcTemplate; +import org.l2jmobius.gameserver.model.holders.DropGroupHolder; import org.l2jmobius.gameserver.model.holders.DropHolder; import org.l2jmobius.gameserver.model.item.ItemTemplate; import org.l2jmobius.gameserver.model.itemcontainer.Inventory; @@ -104,6 +105,17 @@ public class DropSearchBoard implements IParseBoardHandler private void buildDropIndex() { + NpcData.getInstance().getTemplates(npc -> npc.getDropGroups() != null).forEach(npcTemplate -> + { + for (DropGroupHolder dropGroup : npcTemplate.getDropGroups()) + { + final double chance = dropGroup.getChance() / 100; + for (DropHolder dropHolder : dropGroup.getDropList()) + { + addToDropList(npcTemplate, new DropHolder(dropHolder.getDropType(), dropHolder.getItemId(), dropHolder.getMin(), dropHolder.getMax(), dropHolder.getChance() * chance)); + } + } + }); NpcData.getInstance().getTemplates(npc -> npc.getDropList() != null).forEach(npcTemplate -> { for (DropHolder dropHolder : npcTemplate.getDropList()) diff --git a/L2J_Mobius_Essence_6.2_Vanguard/dist/game/data/xsd/npcs.xsd b/L2J_Mobius_Essence_6.2_Vanguard/dist/game/data/xsd/npcs.xsd index ac78c2d82f..9392f96cd7 100644 --- a/L2J_Mobius_Essence_6.2_Vanguard/dist/game/data/xsd/npcs.xsd +++ b/L2J_Mobius_Essence_6.2_Vanguard/dist/game/data/xsd/npcs.xsd @@ -1,16 +1,5 @@ - - - - - - - - - - - @@ -261,11 +250,81 @@ - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/L2J_Mobius_Essence_6.2_Vanguard/java/org/l2jmobius/gameserver/data/xml/NpcData.java b/L2J_Mobius_Essence_6.2_Vanguard/java/org/l2jmobius/gameserver/data/xml/NpcData.java index 205b5e56ae..45b18e8cce 100644 --- a/L2J_Mobius_Essence_6.2_Vanguard/java/org/l2jmobius/gameserver/data/xml/NpcData.java +++ b/L2J_Mobius_Essence_6.2_Vanguard/java/org/l2jmobius/gameserver/data/xml/NpcData.java @@ -47,6 +47,7 @@ import org.l2jmobius.gameserver.enums.MpRewardType; import org.l2jmobius.gameserver.model.StatSet; import org.l2jmobius.gameserver.model.actor.templates.NpcTemplate; import org.l2jmobius.gameserver.model.effects.EffectType; +import org.l2jmobius.gameserver.model.holders.DropGroupHolder; import org.l2jmobius.gameserver.model.holders.DropHolder; import org.l2jmobius.gameserver.model.itemcontainer.Inventory; import org.l2jmobius.gameserver.model.skill.Skill; @@ -104,6 +105,7 @@ public class NpcData implements IXmlReader Set clans = null; Set ignoreClanNpcIds = null; List dropLists = null; + List dropGroups = null; set.set("id", npcId); set.set("displayId", parseInteger(attrs, "displayId")); set.set("level", level); @@ -442,24 +444,66 @@ public class NpcData implements IXmlReader if (dropType != null) { - if (dropLists == null) - { - dropLists = new ArrayList<>(); - } - for (Node dropNode = dropListsNode.getFirstChild(); dropNode != null; dropNode = dropNode.getNextSibling()) { - final NamedNodeMap dropAttrs = dropNode.getAttributes(); - if ("item".equalsIgnoreCase(dropNode.getNodeName())) + final String nodeName = dropNode.getNodeName(); + if (nodeName.equalsIgnoreCase("group")) { - final DropHolder dropItem = new DropHolder(dropType, parseInteger(dropAttrs, "id"), parseLong(dropAttrs, "min"), parseLong(dropAttrs, "max"), parseDouble(dropAttrs, "chance")); - if (ItemTable.getInstance().getTemplate(parseInteger(dropAttrs, "id")) == null) + if (dropGroups == null) { - LOGGER.warning("DropListItem: Could not find item with id " + parseInteger(dropAttrs, "id") + "."); + dropGroups = new ArrayList<>(); + } + + final DropGroupHolder group = new DropGroupHolder(parseDouble(dropNode.getAttributes(), "chance")); + for (Node groupNode = dropNode.getFirstChild(); groupNode != null; groupNode = groupNode.getNextSibling()) + { + if (groupNode.getNodeName().equalsIgnoreCase("item")) + { + final NamedNodeMap groupAttrs = groupNode.getAttributes(); + final int itemId = parseInteger(groupAttrs, "id"); + + // Drop materials for random craft configuration. + if (!Config.DROP_RANDOM_CRAFT_MATERIALS && (itemId >= 92908) && (itemId <= 92919)) + { + continue; + } + + if (ItemTable.getInstance().getTemplate(itemId) == null) + { + LOGGER.warning("DropListItem: Could not find item with id " + itemId + "."); + } + else + { + group.addDrop(new DropHolder(dropType, itemId, parseLong(groupAttrs, "min"), parseLong(groupAttrs, "max"), parseDouble(groupAttrs, "chance"))); + } + } + } + + dropGroups.add(group); + } + else if (nodeName.equalsIgnoreCase("item")) + { + if (dropLists == null) + { + dropLists = new ArrayList<>(); + } + + final NamedNodeMap dropAttrs = dropNode.getAttributes(); + final int itemId = parseInteger(dropAttrs, "id"); + + // Drop materials for random craft configuration. + if (!Config.DROP_RANDOM_CRAFT_MATERIALS && (itemId >= 92908) && (itemId <= 92919)) + { + continue; + } + + if (ItemTable.getInstance().getTemplate(itemId) == null) + { + LOGGER.warning("DropListItem: Could not find item with id " + itemId + "."); } else { - dropLists.add(dropItem); + dropLists.add(new DropHolder(dropType, itemId, parseLong(dropAttrs, "min"), parseLong(dropAttrs, "max"), parseDouble(dropAttrs, "chance"))); } } } @@ -618,32 +662,45 @@ public class NpcData implements IXmlReader template.setClans(clans); template.setIgnoreClanNpcIds(ignoreClanNpcIds); + // Clean old drop groups. + template.removeDropGroups(); + + // Set new drop groups. + if (dropGroups != null) + { + template.setDropGroups(dropGroups); + } + + // Clean old drop lists. + template.removeDrops(); + + // Add LCoin drop for bosses. + if (type.contains("boss")) + { + if (dropLists == null) + { + dropLists = new ArrayList<>(); + } + dropLists.add(new DropHolder(DropType.DROP, Inventory.LCOIN_ID, 1, 1, 100)); + } + + // Add configurable LCoin drop for monsters. + if ((Config.LCOIN_DROP_ENABLED) && (type.contains("Monster") && !type.contains("boss")) && (level >= Config.LCOIN_MIN_MOB_LV)) + { + if (dropLists == null) + { + dropLists = new ArrayList<>(); + } + dropLists.add(new DropHolder(DropType.DROP, Inventory.LCOIN_ID, Config.LCOIN_MIN_QUANTITY, Config.LCOIN_MAX_QUANTITY, Config.LCOIN_DROP_CHANCE)); + } + + // Set new drop lists. if (dropLists != null) { - template.removeDrops(); - - // Add LCoin drop for bosses. - if (type.contains("boss")) - { - dropLists.add(new DropHolder(DropType.DROP, Inventory.LCOIN_ID, 1, 1, 100)); - } - - // Add configurable LCoin drop for monsters. - if ((Config.LCOIN_DROP_ENABLED) && (type.contains("Monster") && !type.contains("boss")) && (level >= Config.LCOIN_MIN_MOB_LV)) - { - dropLists.add(new DropHolder(DropType.DROP, Inventory.LCOIN_ID, Config.LCOIN_MIN_QUANTITY, Config.LCOIN_MAX_QUANTITY, Config.LCOIN_DROP_CHANCE)); - } - // Drops are sorted by chance (high to low). Collections.sort(dropLists, (d1, d2) -> Double.valueOf(d2.getChance()).compareTo(Double.valueOf(d1.getChance()))); for (DropHolder dropHolder : dropLists) { - // Drop materials for random craft configuration. - if (!Config.DROP_RANDOM_CRAFT_MATERIALS && (dropHolder.getItemId() >= 92908) && (dropHolder.getItemId() <= 92919)) - { - continue; - } - switch (dropHolder.getDropType()) { case DROP: diff --git a/L2J_Mobius_Essence_6.2_Vanguard/java/org/l2jmobius/gameserver/model/actor/templates/NpcTemplate.java b/L2J_Mobius_Essence_6.2_Vanguard/java/org/l2jmobius/gameserver/model/actor/templates/NpcTemplate.java index 173ee61113..ffe745209f 100644 --- a/L2J_Mobius_Essence_6.2_Vanguard/java/org/l2jmobius/gameserver/model/actor/templates/NpcTemplate.java +++ b/L2J_Mobius_Essence_6.2_Vanguard/java/org/l2jmobius/gameserver/model/actor/templates/NpcTemplate.java @@ -37,6 +37,7 @@ import org.l2jmobius.gameserver.enums.Race; import org.l2jmobius.gameserver.enums.Sex; import org.l2jmobius.gameserver.model.StatSet; import org.l2jmobius.gameserver.model.actor.Creature; +import org.l2jmobius.gameserver.model.holders.DropGroupHolder; import org.l2jmobius.gameserver.model.holders.DropHolder; import org.l2jmobius.gameserver.model.holders.ItemHolder; import org.l2jmobius.gameserver.model.interfaces.IIdentifiable; @@ -108,6 +109,7 @@ public class NpcTemplate extends CreatureTemplate implements IIdentifiable private Map> _aiSkillLists; private Set _clans; private Set _ignoreClanNpcIds; + private List _dropGroups; private List _dropListDeath; private List _dropListSpoil; private float _collisionRadiusGrown; @@ -664,12 +666,22 @@ public class NpcTemplate extends CreatureTemplate implements IIdentifiable _ignoreClanNpcIds = ignoreClanNpcIds != null ? Collections.unmodifiableSet(ignoreClanNpcIds) : null; } + public void removeDropGroups() + { + _dropGroups = null; + } + public void removeDrops() { _dropListDeath = null; _dropListSpoil = null; } + public void setDropGroups(List groups) + { + _dropGroups = groups; + } + public void addDrop(DropHolder dropHolder) { if (_dropListDeath == null) @@ -688,6 +700,11 @@ public class NpcTemplate extends CreatureTemplate implements IIdentifiable _dropListSpoil.add(dropHolder); } + public List getDropGroups() + { + return _dropGroups; + } + public List getDropList() { return _dropListDeath; @@ -700,11 +717,201 @@ public class NpcTemplate extends CreatureTemplate implements IIdentifiable public List calculateDrops(DropType dropType, Creature victim, Creature killer) { - final List dropList = dropType == DropType.SPOIL ? _dropListSpoil : _dropListDeath; - if (dropList == null) + if (dropType == DropType.DROP) { - return null; + // calculate group drops + List groupDrops = null; + if (_dropGroups != null) + { + groupDrops = calculateGroupDrops(victim, killer); + } + + // calculate ungrouped drops + List ungroupedDrops = null; + if (_dropListDeath != null) + { + ungroupedDrops = calculateUngroupedDrops(dropType, victim, killer); + } + + // return results + if ((groupDrops != null) && (ungroupedDrops != null)) + { + groupDrops.addAll(ungroupedDrops); + ungroupedDrops.clear(); + return groupDrops; + } + if (groupDrops != null) + { + return groupDrops; + } + if (ungroupedDrops != null) + { + return ungroupedDrops; + } } + else if ((dropType == DropType.SPOIL) && (_dropListSpoil != null)) + { + return calculateUngroupedDrops(dropType, victim, killer); + } + + // no drops + return null; + } + + private List calculateGroupDrops(Creature victim, Creature killer) + { + // level difference calculations + final int levelDifference = victim.getLevel() - killer.getLevel(); + final double levelGapChanceToDropAdena = Util.map(levelDifference, -Config.DROP_ADENA_MAX_LEVEL_DIFFERENCE, -Config.DROP_ADENA_MIN_LEVEL_DIFFERENCE, Config.DROP_ADENA_MIN_LEVEL_GAP_CHANCE, 100d); + final double levelGapChanceToDrop = Util.map(levelDifference, -Config.DROP_ITEM_MAX_LEVEL_DIFFERENCE, -Config.DROP_ITEM_MIN_LEVEL_DIFFERENCE, Config.DROP_ITEM_MIN_LEVEL_GAP_CHANCE, 100d); + + int dropOccurrenceCounter = victim.isRaid() ? Config.DROP_MAX_OCCURRENCES_RAIDBOSS : Config.DROP_MAX_OCCURRENCES_NORMAL; + List calculatedDrops = null; + for (DropGroupHolder group : _dropGroups) + { + if (dropOccurrenceCounter <= 0) + { + break; + } + + double groupRate = 1; + final List groupDrops = group.getDropList(); + for (DropHolder dropItem : groupDrops) + { + final int itemId = dropItem.getItemId(); + final ItemTemplate item = ItemTable.getInstance().getTemplate(itemId); + final boolean champion = victim.isChampion(); + + // chance + double rateChance = 1; + final Float itemChance = Config.RATE_DROP_CHANCE_BY_ID.get(itemId); + if (itemChance != null) + { + if (itemChance == 0) + { + continue; + } + + rateChance *= itemChance; + if (champion && (itemId == Inventory.ADENA_ID)) + { + rateChance *= Config.CHAMPION_ADENAS_REWARDS_CHANCE; + } + } + else if (item.hasExImmediateEffect()) + { + rateChance *= Config.RATE_HERB_DROP_CHANCE_MULTIPLIER; + } + else if (victim.isRaid()) + { + rateChance *= Config.RATE_RAID_DROP_CHANCE_MULTIPLIER; + } + else + { + rateChance *= Config.RATE_DEATH_DROP_CHANCE_MULTIPLIER * (champion ? Config.CHAMPION_REWARDS_CHANCE : 1); + } + + // premium chance + if (Config.PREMIUM_SYSTEM_ENABLED && (killer.getActingPlayer() != null) && killer.getActingPlayer().hasPremiumStatus()) + { + if (Config.PREMIUM_RATE_DROP_CHANCE_BY_ID.get(itemId) != null) + { + rateChance *= Config.PREMIUM_RATE_DROP_CHANCE_BY_ID.get(itemId); + } + else if (item.hasExImmediateEffect()) + { + // TODO: Premium herb chance? :) + } + else if (victim.isRaid()) + { + // TODO: Premium raid chance? :) + } + else + { + rateChance *= Config.PREMIUM_RATE_DROP_CHANCE; + } + } + + // keep lowest to avoid chance by id configuration conflicts + groupRate = Math.min(groupRate, rateChance); + } + + if ((Rnd.nextDouble() * 100) < (group.getChance() * groupRate)) + { + double totalChance = 0; // total group chance is 100 + final double dropChance = Rnd.nextDouble() * 100; + GROUP_DROP: for (DropHolder dropItem : groupDrops) + { + if (dropOccurrenceCounter <= 0) + { + break; + } + + // calculate if item will drop + totalChance += dropItem.getChance(); + if (dropChance >= totalChance) + { + continue; + } + + // check level gap that may prevent to drop item + if ((Rnd.nextDouble() * 100) > (dropItem.getItemId() == Inventory.ADENA_ID ? levelGapChanceToDropAdena : levelGapChanceToDrop)) + { + continue; + } + + // create the drop + final ItemHolder drop = calculateGroupDrop(dropItem, victim, killer); + + // create list + if (calculatedDrops == null) + { + calculatedDrops = new ArrayList<>(dropOccurrenceCounter); + } + + // finally + if (group.getChance() < 100) + { + dropOccurrenceCounter--; + } + calculatedDrops.add(drop); + + // no more drops from this group + break GROUP_DROP; + } + } + } + + // champion extra drop + if (victim.isChampion()) + { + if ((victim.getLevel() < killer.getLevel()) && (Rnd.get(100) < Config.CHAMPION_REWARD_LOWER_LEVEL_ITEM_CHANCE)) + { + return calculatedDrops; + } + if ((victim.getLevel() > killer.getLevel()) && (Rnd.get(100) < Config.CHAMPION_REWARD_HIGHER_LEVEL_ITEM_CHANCE)) + { + return calculatedDrops; + } + + // create list + if (calculatedDrops == null) + { + calculatedDrops = new ArrayList<>(); + } + + if (!calculatedDrops.containsAll(Config.CHAMPION_REWARD_ITEMS)) + { + calculatedDrops.addAll(Config.CHAMPION_REWARD_ITEMS); + } + } + + return calculatedDrops; + } + + private List calculateUngroupedDrops(DropType dropType, Creature victim, Creature killer) + { + final List dropList = dropType == DropType.SPOIL ? _dropListSpoil : _dropListDeath; // level difference calculations final int levelDifference = victim.getLevel() - killer.getLevel(); @@ -736,7 +943,7 @@ public class NpcTemplate extends CreatureTemplate implements IIdentifiable } // calculate chances - final ItemHolder drop = calculateDrop(dropItem, victim, killer); + final ItemHolder drop = calculateUngroupedDrop(dropItem, victim, killer); if (drop == null) { continue; @@ -791,20 +998,82 @@ public class NpcTemplate extends CreatureTemplate implements IIdentifiable calculatedDrops = new ArrayList<>(); } - calculatedDrops.addAll(Config.CHAMPION_REWARD_ITEMS); + if (!calculatedDrops.containsAll(Config.CHAMPION_REWARD_ITEMS)) + { + calculatedDrops.addAll(Config.CHAMPION_REWARD_ITEMS); + } } return calculatedDrops; } /** - * All item drop chance calculations are done by this method. * @param dropItem * @param victim * @param killer * @return ItemHolder */ - private ItemHolder calculateDrop(DropHolder dropItem, Creature victim, Creature killer) + private ItemHolder calculateGroupDrop(DropHolder dropItem, Creature victim, Creature killer) + { + final int itemId = dropItem.getItemId(); + final ItemTemplate item = ItemTable.getInstance().getTemplate(itemId); + final boolean champion = victim.isChampion(); + + // calculate amount + double rateAmount = 1; + if (Config.RATE_DROP_AMOUNT_BY_ID.get(itemId) != null) + { + rateAmount *= Config.RATE_DROP_AMOUNT_BY_ID.get(itemId); + if (champion && (itemId == Inventory.ADENA_ID)) + { + rateAmount *= Config.CHAMPION_ADENAS_REWARDS_AMOUNT; + } + } + else if (item.hasExImmediateEffect()) + { + rateAmount *= Config.RATE_HERB_DROP_AMOUNT_MULTIPLIER; + } + else if (victim.isRaid()) + { + rateAmount *= Config.RATE_RAID_DROP_AMOUNT_MULTIPLIER; + } + else + { + rateAmount *= Config.RATE_DEATH_DROP_AMOUNT_MULTIPLIER * (champion ? Config.CHAMPION_REWARDS_AMOUNT : 1); + } + + // premium amount + if (Config.PREMIUM_SYSTEM_ENABLED && (killer.getActingPlayer() != null) && killer.getActingPlayer().hasPremiumStatus()) + { + if (Config.PREMIUM_RATE_DROP_AMOUNT_BY_ID.get(itemId) != null) + { + rateAmount *= Config.PREMIUM_RATE_DROP_AMOUNT_BY_ID.get(itemId); + } + else if (item.hasExImmediateEffect()) + { + // TODO: Premium herb amount? :) + } + else if (victim.isRaid()) + { + // TODO: Premium raid amount? :) + } + else + { + rateAmount *= Config.PREMIUM_RATE_DROP_AMOUNT; + } + } + + // finally + return new ItemHolder(itemId, (long) (Rnd.get(dropItem.getMin(), dropItem.getMax()) * rateAmount)); + } + + /** + * @param dropItem + * @param victim + * @param killer + * @return ItemHolder + */ + private ItemHolder calculateUngroupedDrop(DropHolder dropItem, Creature victim, Creature killer) { switch (dropItem.getDropType()) { diff --git a/L2J_Mobius_Essence_6.2_Vanguard/java/org/l2jmobius/gameserver/model/holders/DropGroupHolder.java b/L2J_Mobius_Essence_6.2_Vanguard/java/org/l2jmobius/gameserver/model/holders/DropGroupHolder.java new file mode 100644 index 0000000000..6c2401c0b0 --- /dev/null +++ b/L2J_Mobius_Essence_6.2_Vanguard/java/org/l2jmobius/gameserver/model/holders/DropGroupHolder.java @@ -0,0 +1,49 @@ +/* + * This file is part of the L2J Mobius project. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package org.l2jmobius.gameserver.model.holders; + +import java.util.ArrayList; +import java.util.List; + +/** + * @author Mobius + */ +public class DropGroupHolder +{ + private final List _dropList = new ArrayList<>(); + private final double _chance; + + public DropGroupHolder(double chance) + { + _chance = chance; + } + + public List getDropList() + { + return _dropList; + } + + public void addDrop(DropHolder holder) + { + _dropList.add(holder); + } + + public double getChance() + { + return _chance; + } +}