Support for drop groups.

This commit is contained in:
MobiusDevelopment 2022-04-17 11:38:41 +00:00
parent 4b2456a2e2
commit 6fc3b9073a
150 changed files with 12664 additions and 1073 deletions

View File

@ -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<DropGroupHolder> dropListGroups = npc.getTemplate().getDropGroups();
final List<DropHolder> dropListDeath = npc.getTemplate().getDropList();
final List<DropHolder> dropListSpoil = npc.getTemplate().getSpoilList();
if ((dropListDeath != null) || (dropListSpoil != null))
if ((dropListGroups != null) || (dropListDeath != null) || (dropListSpoil != null))
{
sb.append("<table width=275 cellpadding=0 cellspacing=0><tr>");
if (dropListDeath != null)
if ((dropListGroups != null) || (dropListDeath != null))
{
sb.append("<td align=center><button value=\"Show Drop\" width=100 height=25 action=\"bypass NpcViewMod dropList DROP " + npc.getObjectId() + "\" back=\"L2UI_CT1.Button_DF_Calculator_Down\" fore=\"L2UI_CT1.Button_DF_Calculator\"></td>");
}
@ -348,13 +350,40 @@ public class NpcViewMod implements IBypassHandler
private void sendNpcDropList(Player player, Npc npc, DropType dropType, int pageValue)
{
final List<DropHolder> templateList = dropType == DropType.SPOIL ? npc.getTemplate().getSpoilList() : npc.getTemplate().getDropList();
if (templateList == null)
List<DropHolder> dropList = null;
if (dropType == DropType.SPOIL)
{
dropList = new ArrayList<>(npc.getTemplate().getSpoilList());
}
else
{
final List<DropHolder> drops = npc.getTemplate().getDropList();
if (drops != null)
{
dropList = new ArrayList<>(drops);
}
final List<DropGroupHolder> 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<DropHolder> 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;

View File

@ -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())

View File

@ -1,16 +1,5 @@
<?xml version="1.0" encoding="UTF-8"?>
<xs:schema xmlns:xs="http://www.w3.org/2001/XMLSchema">
<xs:complexType name="dropListItem">
<xs:attribute name="id" type="xs:positiveInteger" use="required" />
<xs:attribute name="min" type="xs:nonNegativeInteger" />
<xs:attribute name="max" type="xs:positiveInteger" />
<xs:attribute name="chance" type="xs:decimal" />
</xs:complexType>
<xs:complexType name="dropList">
<xs:choice minOccurs="1" maxOccurs="unbounded">
<xs:element name="item" type="dropListItem" />
</xs:choice>
</xs:complexType>
<xs:element name="list">
<xs:complexType>
<xs:sequence>
@ -260,11 +249,81 @@
</xs:element>
<xs:element name="dropLists" minOccurs="0" maxOccurs="1">
<xs:complexType>
<xs:all>
<xs:element name="drop" type="dropList" minOccurs="0" maxOccurs="1" />
<xs:element name="spoil" type="dropList" minOccurs="0" maxOccurs="1" />
<xs:element name="lucky" type="dropList" minOccurs="0" maxOccurs="1" />
</xs:all>
<xs:sequence>
<xs:element name="drop" minOccurs="0">
<xs:complexType>
<xs:sequence>
<xs:element name="item" maxOccurs="unbounded" minOccurs="0">
<xs:complexType>
<xs:simpleContent>
<xs:extension base="xs:string">
<xs:attribute type="xs:int" name="id" use="required"/>
<xs:attribute type="xs:long" name="min" use="required"/>
<xs:attribute type="xs:long" name="max" use="required"/>
<xs:attribute type="xs:double" name="chance" use="required"/>
</xs:extension>
</xs:simpleContent>
</xs:complexType>
</xs:element>
<xs:element name="group" maxOccurs="unbounded" minOccurs="0">
<xs:complexType>
<xs:sequence>
<xs:element name="item" maxOccurs="unbounded" minOccurs="0">
<xs:complexType>
<xs:simpleContent>
<xs:extension base="xs:string">
<xs:attribute type="xs:int" name="id" use="required"/>
<xs:attribute type="xs:long" name="min" use="required"/>
<xs:attribute type="xs:long" name="max" use="required"/>
<xs:attribute type="xs:double" name="chance" use="required"/>
</xs:extension>
</xs:simpleContent>
</xs:complexType>
</xs:element>
</xs:sequence>
<xs:attribute type="xs:double" name="chance" use="optional"/>
</xs:complexType>
</xs:element>
</xs:sequence>
</xs:complexType>
</xs:element>
<xs:element name="spoil" minOccurs="0">
<xs:complexType>
<xs:sequence>
<xs:element name="item" maxOccurs="unbounded" minOccurs="0">
<xs:complexType>
<xs:simpleContent>
<xs:extension base="xs:string">
<xs:attribute type="xs:int" name="id" use="optional"/>
<xs:attribute type="xs:long" name="min" use="optional"/>
<xs:attribute type="xs:long" name="max" use="optional"/>
<xs:attribute type="xs:double" name="chance" use="optional"/>
</xs:extension>
</xs:simpleContent>
</xs:complexType>
</xs:element>
</xs:sequence>
</xs:complexType>
</xs:element>
<xs:element name="lucky" minOccurs="0">
<xs:complexType>
<xs:sequence>
<xs:element name="item" maxOccurs="unbounded" minOccurs="0">
<xs:complexType>
<xs:simpleContent>
<xs:extension base="xs:string">
<xs:attribute type="xs:int" name="id" use="optional"/>
<xs:attribute type="xs:long" name="min" use="optional"/>
<xs:attribute type="xs:long" name="max" use="optional"/>
<xs:attribute type="xs:double" name="chance" use="optional"/>
</xs:extension>
</xs:simpleContent>
</xs:complexType>
</xs:element>
</xs:sequence>
</xs:complexType>
</xs:element>
</xs:sequence>
</xs:complexType>
</xs:element>
<xs:element name="collision" minOccurs="0" maxOccurs="1">

View File

@ -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<Integer> clans = null;
Set<Integer> ignoreClanNpcIds = null;
List<DropHolder> dropLists = null;
List<DropGroupHolder> 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)

View File

@ -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<AISkillScope, List<Skill>> _aiSkillLists;
private Set<Integer> _clans;
private Set<Integer> _ignoreClanNpcIds;
private List<DropGroupHolder> _dropGroups;
private List<DropHolder> _dropListDeath;
private List<DropHolder> _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<DropGroupHolder> 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<DropGroupHolder> getDropGroups()
{
return _dropGroups;
}
public List<DropHolder> getDropList()
{
return _dropListDeath;
@ -685,11 +702,201 @@ public class NpcTemplate extends CreatureTemplate implements IIdentifiable
public List<ItemHolder> calculateDrops(DropType dropType, Creature victim, Creature killer)
{
final List<DropHolder> dropList = dropType == DropType.SPOIL ? _dropListSpoil : _dropListDeath;
if (dropList == null)
if (dropType == DropType.DROP)
{
return null;
// calculate group drops
List<ItemHolder> groupDrops = null;
if (_dropGroups != null)
{
groupDrops = calculateGroupDrops(victim, killer);
}
// calculate ungrouped drops
List<ItemHolder> 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<ItemHolder> 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<ItemHolder> calculatedDrops = null;
for (DropGroupHolder group : _dropGroups)
{
if (dropOccurrenceCounter <= 0)
{
break;
}
double groupRate = 1;
final List<DropHolder> 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<ItemHolder> calculateUngroupedDrops(DropType dropType, Creature victim, Creature killer)
{
final List<DropHolder> 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())
{

View File

@ -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 <http://www.gnu.org/licenses/>.
*/
package org.l2jmobius.gameserver.model.holders;
import java.util.ArrayList;
import java.util.List;
/**
* @author Mobius
*/
public class DropGroupHolder
{
private final List<DropHolder> _dropList = new ArrayList<>();
private final double _chance;
public DropGroupHolder(double chance)
{
_chance = chance;
}
public List<DropHolder> getDropList()
{
return _dropList;
}
public void addDrop(DropHolder holder)
{
_dropList.add(holder);
}
public double getChance()
{
return _chance;
}
}

View File

@ -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<DropGroupHolder> dropListGroups = npc.getTemplate().getDropGroups();
final List<DropHolder> dropListDeath = npc.getTemplate().getDropList();
final List<DropHolder> dropListSpoil = npc.getTemplate().getSpoilList();
if ((dropListDeath != null) || (dropListSpoil != null))
if ((dropListGroups != null) || (dropListDeath != null) || (dropListSpoil != null))
{
sb.append("<table width=275 cellpadding=0 cellspacing=0><tr>");
if (dropListDeath != null)
if ((dropListGroups != null) || (dropListDeath != null))
{
sb.append("<td align=center><button value=\"Show Drop\" width=100 height=25 action=\"bypass NpcViewMod dropList DROP " + npc.getObjectId() + "\" back=\"L2UI_CT1.Button_DF_Calculator_Down\" fore=\"L2UI_CT1.Button_DF_Calculator\"></td>");
}
@ -348,13 +350,40 @@ public class NpcViewMod implements IBypassHandler
private void sendNpcDropList(Player player, Npc npc, DropType dropType, int pageValue)
{
final List<DropHolder> templateList = dropType == DropType.SPOIL ? npc.getTemplate().getSpoilList() : npc.getTemplate().getDropList();
if (templateList == null)
List<DropHolder> dropList = null;
if (dropType == DropType.SPOIL)
{
dropList = new ArrayList<>(npc.getTemplate().getSpoilList());
}
else
{
final List<DropHolder> drops = npc.getTemplate().getDropList();
if (drops != null)
{
dropList = new ArrayList<>(drops);
}
final List<DropGroupHolder> 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<DropHolder> 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;

View File

@ -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())

View File

@ -1,16 +1,5 @@
<?xml version="1.0" encoding="UTF-8"?>
<xs:schema xmlns:xs="http://www.w3.org/2001/XMLSchema">
<xs:complexType name="dropListItem">
<xs:attribute name="id" type="xs:positiveInteger" use="required" />
<xs:attribute name="min" type="xs:nonNegativeInteger" />
<xs:attribute name="max" type="xs:positiveInteger" />
<xs:attribute name="chance" type="xs:decimal" />
</xs:complexType>
<xs:complexType name="dropList">
<xs:choice minOccurs="1" maxOccurs="unbounded">
<xs:element name="item" type="dropListItem" />
</xs:choice>
</xs:complexType>
<xs:element name="list">
<xs:complexType>
<xs:sequence>
@ -260,11 +249,81 @@
</xs:element>
<xs:element name="dropLists" minOccurs="0" maxOccurs="1">
<xs:complexType>
<xs:all>
<xs:element name="drop" type="dropList" minOccurs="0" maxOccurs="1" />
<xs:element name="spoil" type="dropList" minOccurs="0" maxOccurs="1" />
<xs:element name="lucky" type="dropList" minOccurs="0" maxOccurs="1" />
</xs:all>
<xs:sequence>
<xs:element name="drop" minOccurs="0">
<xs:complexType>
<xs:sequence>
<xs:element name="item" maxOccurs="unbounded" minOccurs="0">
<xs:complexType>
<xs:simpleContent>
<xs:extension base="xs:string">
<xs:attribute type="xs:int" name="id" use="required"/>
<xs:attribute type="xs:long" name="min" use="required"/>
<xs:attribute type="xs:long" name="max" use="required"/>
<xs:attribute type="xs:double" name="chance" use="required"/>
</xs:extension>
</xs:simpleContent>
</xs:complexType>
</xs:element>
<xs:element name="group" maxOccurs="unbounded" minOccurs="0">
<xs:complexType>
<xs:sequence>
<xs:element name="item" maxOccurs="unbounded" minOccurs="0">
<xs:complexType>
<xs:simpleContent>
<xs:extension base="xs:string">
<xs:attribute type="xs:int" name="id" use="required"/>
<xs:attribute type="xs:long" name="min" use="required"/>
<xs:attribute type="xs:long" name="max" use="required"/>
<xs:attribute type="xs:double" name="chance" use="required"/>
</xs:extension>
</xs:simpleContent>
</xs:complexType>
</xs:element>
</xs:sequence>
<xs:attribute type="xs:double" name="chance" use="optional"/>
</xs:complexType>
</xs:element>
</xs:sequence>
</xs:complexType>
</xs:element>
<xs:element name="spoil" minOccurs="0">
<xs:complexType>
<xs:sequence>
<xs:element name="item" maxOccurs="unbounded" minOccurs="0">
<xs:complexType>
<xs:simpleContent>
<xs:extension base="xs:string">
<xs:attribute type="xs:int" name="id" use="optional"/>
<xs:attribute type="xs:long" name="min" use="optional"/>
<xs:attribute type="xs:long" name="max" use="optional"/>
<xs:attribute type="xs:double" name="chance" use="optional"/>
</xs:extension>
</xs:simpleContent>
</xs:complexType>
</xs:element>
</xs:sequence>
</xs:complexType>
</xs:element>
<xs:element name="lucky" minOccurs="0">
<xs:complexType>
<xs:sequence>
<xs:element name="item" maxOccurs="unbounded" minOccurs="0">
<xs:complexType>
<xs:simpleContent>
<xs:extension base="xs:string">
<xs:attribute type="xs:int" name="id" use="optional"/>
<xs:attribute type="xs:long" name="min" use="optional"/>
<xs:attribute type="xs:long" name="max" use="optional"/>
<xs:attribute type="xs:double" name="chance" use="optional"/>
</xs:extension>
</xs:simpleContent>
</xs:complexType>
</xs:element>
</xs:sequence>
</xs:complexType>
</xs:element>
</xs:sequence>
</xs:complexType>
</xs:element>
<xs:element name="collision" minOccurs="0" maxOccurs="1">

View File

@ -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<Integer> clans = null;
Set<Integer> ignoreClanNpcIds = null;
List<DropHolder> dropLists = null;
List<DropGroupHolder> 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)

View File

@ -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<AISkillScope, List<Skill>> _aiSkillLists;
private Set<Integer> _clans;
private Set<Integer> _ignoreClanNpcIds;
private List<DropGroupHolder> _dropGroups;
private List<DropHolder> _dropListDeath;
private List<DropHolder> _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<DropGroupHolder> 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<DropGroupHolder> getDropGroups()
{
return _dropGroups;
}
public List<DropHolder> getDropList()
{
return _dropListDeath;
@ -685,11 +702,201 @@ public class NpcTemplate extends CreatureTemplate implements IIdentifiable
public List<ItemHolder> calculateDrops(DropType dropType, Creature victim, Creature killer)
{
final List<DropHolder> dropList = dropType == DropType.SPOIL ? _dropListSpoil : _dropListDeath;
if (dropList == null)
if (dropType == DropType.DROP)
{
return null;
// calculate group drops
List<ItemHolder> groupDrops = null;
if (_dropGroups != null)
{
groupDrops = calculateGroupDrops(victim, killer);
}
// calculate ungrouped drops
List<ItemHolder> 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<ItemHolder> 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<ItemHolder> calculatedDrops = null;
for (DropGroupHolder group : _dropGroups)
{
if (dropOccurrenceCounter <= 0)
{
break;
}
double groupRate = 1;
final List<DropHolder> 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<ItemHolder> calculateUngroupedDrops(DropType dropType, Creature victim, Creature killer)
{
final List<DropHolder> 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())
{

View File

@ -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 <http://www.gnu.org/licenses/>.
*/
package org.l2jmobius.gameserver.model.holders;
import java.util.ArrayList;
import java.util.List;
/**
* @author Mobius
*/
public class DropGroupHolder
{
private final List<DropHolder> _dropList = new ArrayList<>();
private final double _chance;
public DropGroupHolder(double chance)
{
_chance = chance;
}
public List<DropHolder> getDropList()
{
return _dropList;
}
public void addDrop(DropHolder holder)
{
_dropList.add(holder);
}
public double getChance()
{
return _chance;
}
}

View File

@ -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<DropGroupHolder> dropListGroups = npc.getTemplate().getDropGroups();
final List<DropHolder> dropListDeath = npc.getTemplate().getDropList();
final List<DropHolder> dropListSpoil = npc.getTemplate().getSpoilList();
if ((dropListDeath != null) || (dropListSpoil != null))
if ((dropListGroups != null) || (dropListDeath != null) || (dropListSpoil != null))
{
sb.append("<table width=275 cellpadding=0 cellspacing=0><tr>");
if (dropListDeath != null)
if ((dropListGroups != null) || (dropListDeath != null))
{
sb.append("<td align=center><button value=\"Show Drop\" width=100 height=25 action=\"bypass NpcViewMod dropList DROP " + npc.getObjectId() + "\" back=\"L2UI_CT1.Button_DF_Calculator_Down\" fore=\"L2UI_CT1.Button_DF_Calculator\"></td>");
}
@ -348,13 +350,40 @@ public class NpcViewMod implements IBypassHandler
private void sendNpcDropList(Player player, Npc npc, DropType dropType, int pageValue)
{
final List<DropHolder> templateList = dropType == DropType.SPOIL ? npc.getTemplate().getSpoilList() : npc.getTemplate().getDropList();
if (templateList == null)
List<DropHolder> dropList = null;
if (dropType == DropType.SPOIL)
{
dropList = new ArrayList<>(npc.getTemplate().getSpoilList());
}
else
{
final List<DropHolder> drops = npc.getTemplate().getDropList();
if (drops != null)
{
dropList = new ArrayList<>(drops);
}
final List<DropGroupHolder> 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<DropHolder> 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;

View File

@ -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())

View File

@ -1,16 +1,5 @@
<?xml version="1.0" encoding="UTF-8"?>
<xs:schema xmlns:xs="http://www.w3.org/2001/XMLSchema">
<xs:complexType name="dropListItem">
<xs:attribute name="id" type="xs:positiveInteger" use="required" />
<xs:attribute name="min" type="xs:nonNegativeInteger" />
<xs:attribute name="max" type="xs:positiveInteger" />
<xs:attribute name="chance" type="xs:decimal" />
</xs:complexType>
<xs:complexType name="dropList">
<xs:choice minOccurs="1" maxOccurs="unbounded">
<xs:element name="item" type="dropListItem" />
</xs:choice>
</xs:complexType>
<xs:element name="list">
<xs:complexType>
<xs:sequence>
@ -260,11 +249,81 @@
</xs:element>
<xs:element name="dropLists" minOccurs="0" maxOccurs="1">
<xs:complexType>
<xs:all>
<xs:element name="drop" type="dropList" minOccurs="0" maxOccurs="1" />
<xs:element name="spoil" type="dropList" minOccurs="0" maxOccurs="1" />
<xs:element name="lucky" type="dropList" minOccurs="0" maxOccurs="1" />
</xs:all>
<xs:sequence>
<xs:element name="drop" minOccurs="0">
<xs:complexType>
<xs:sequence>
<xs:element name="item" maxOccurs="unbounded" minOccurs="0">
<xs:complexType>
<xs:simpleContent>
<xs:extension base="xs:string">
<xs:attribute type="xs:int" name="id" use="required"/>
<xs:attribute type="xs:long" name="min" use="required"/>
<xs:attribute type="xs:long" name="max" use="required"/>
<xs:attribute type="xs:double" name="chance" use="required"/>
</xs:extension>
</xs:simpleContent>
</xs:complexType>
</xs:element>
<xs:element name="group" maxOccurs="unbounded" minOccurs="0">
<xs:complexType>
<xs:sequence>
<xs:element name="item" maxOccurs="unbounded" minOccurs="0">
<xs:complexType>
<xs:simpleContent>
<xs:extension base="xs:string">
<xs:attribute type="xs:int" name="id" use="required"/>
<xs:attribute type="xs:long" name="min" use="required"/>
<xs:attribute type="xs:long" name="max" use="required"/>
<xs:attribute type="xs:double" name="chance" use="required"/>
</xs:extension>
</xs:simpleContent>
</xs:complexType>
</xs:element>
</xs:sequence>
<xs:attribute type="xs:double" name="chance" use="optional"/>
</xs:complexType>
</xs:element>
</xs:sequence>
</xs:complexType>
</xs:element>
<xs:element name="spoil" minOccurs="0">
<xs:complexType>
<xs:sequence>
<xs:element name="item" maxOccurs="unbounded" minOccurs="0">
<xs:complexType>
<xs:simpleContent>
<xs:extension base="xs:string">
<xs:attribute type="xs:int" name="id" use="optional"/>
<xs:attribute type="xs:long" name="min" use="optional"/>
<xs:attribute type="xs:long" name="max" use="optional"/>
<xs:attribute type="xs:double" name="chance" use="optional"/>
</xs:extension>
</xs:simpleContent>
</xs:complexType>
</xs:element>
</xs:sequence>
</xs:complexType>
</xs:element>
<xs:element name="lucky" minOccurs="0">
<xs:complexType>
<xs:sequence>
<xs:element name="item" maxOccurs="unbounded" minOccurs="0">
<xs:complexType>
<xs:simpleContent>
<xs:extension base="xs:string">
<xs:attribute type="xs:int" name="id" use="optional"/>
<xs:attribute type="xs:long" name="min" use="optional"/>
<xs:attribute type="xs:long" name="max" use="optional"/>
<xs:attribute type="xs:double" name="chance" use="optional"/>
</xs:extension>
</xs:simpleContent>
</xs:complexType>
</xs:element>
</xs:sequence>
</xs:complexType>
</xs:element>
</xs:sequence>
</xs:complexType>
</xs:element>
<xs:element name="collision" minOccurs="0" maxOccurs="1">

View File

@ -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<Integer> clans = null;
Set<Integer> ignoreClanNpcIds = null;
List<DropHolder> dropLists = null;
List<DropGroupHolder> 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)

View File

@ -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<AISkillScope, List<Skill>> _aiSkillLists;
private Set<Integer> _clans;
private Set<Integer> _ignoreClanNpcIds;
private List<DropGroupHolder> _dropGroups;
private List<DropHolder> _dropListDeath;
private List<DropHolder> _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<DropGroupHolder> 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<DropGroupHolder> getDropGroups()
{
return _dropGroups;
}
public List<DropHolder> getDropList()
{
return _dropListDeath;
@ -685,11 +702,201 @@ public class NpcTemplate extends CreatureTemplate implements IIdentifiable
public List<ItemHolder> calculateDrops(DropType dropType, Creature victim, Creature killer)
{
final List<DropHolder> dropList = dropType == DropType.SPOIL ? _dropListSpoil : _dropListDeath;
if (dropList == null)
if (dropType == DropType.DROP)
{
return null;
// calculate group drops
List<ItemHolder> groupDrops = null;
if (_dropGroups != null)
{
groupDrops = calculateGroupDrops(victim, killer);
}
// calculate ungrouped drops
List<ItemHolder> 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<ItemHolder> 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<ItemHolder> calculatedDrops = null;
for (DropGroupHolder group : _dropGroups)
{
if (dropOccurrenceCounter <= 0)
{
break;
}
double groupRate = 1;
final List<DropHolder> 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<ItemHolder> calculateUngroupedDrops(DropType dropType, Creature victim, Creature killer)
{
final List<DropHolder> 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())
{

View File

@ -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 <http://www.gnu.org/licenses/>.
*/
package org.l2jmobius.gameserver.model.holders;
import java.util.ArrayList;
import java.util.List;
/**
* @author Mobius
*/
public class DropGroupHolder
{
private final List<DropHolder> _dropList = new ArrayList<>();
private final double _chance;
public DropGroupHolder(double chance)
{
_chance = chance;
}
public List<DropHolder> getDropList()
{
return _dropList;
}
public void addDrop(DropHolder holder)
{
_dropList.add(holder);
}
public double getChance()
{
return _chance;
}
}

View File

@ -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<DropGroupHolder> dropListGroups = npc.getTemplate().getDropGroups();
final List<DropHolder> dropListDeath = npc.getTemplate().getDropList();
final List<DropHolder> dropListSpoil = npc.getTemplate().getSpoilList();
if ((dropListDeath != null) || (dropListSpoil != null))
if ((dropListGroups != null) || (dropListDeath != null) || (dropListSpoil != null))
{
sb.append("<table width=275 cellpadding=0 cellspacing=0><tr>");
if (dropListDeath != null)
if ((dropListGroups != null) || (dropListDeath != null))
{
sb.append("<td align=center><button value=\"Show Drop\" width=100 height=25 action=\"bypass NpcViewMod dropList DROP " + npc.getObjectId() + "\" back=\"L2UI_CT1.Button_DF_Calculator_Down\" fore=\"L2UI_CT1.Button_DF_Calculator\"></td>");
}
@ -348,13 +350,40 @@ public class NpcViewMod implements IBypassHandler
private void sendNpcDropList(Player player, Npc npc, DropType dropType, int pageValue)
{
final List<DropHolder> templateList = dropType == DropType.SPOIL ? npc.getTemplate().getSpoilList() : npc.getTemplate().getDropList();
if (templateList == null)
List<DropHolder> dropList = null;
if (dropType == DropType.SPOIL)
{
dropList = new ArrayList<>(npc.getTemplate().getSpoilList());
}
else
{
final List<DropHolder> drops = npc.getTemplate().getDropList();
if (drops != null)
{
dropList = new ArrayList<>(drops);
}
final List<DropGroupHolder> 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<DropHolder> 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;

View File

@ -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())

View File

@ -1,16 +1,5 @@
<?xml version="1.0" encoding="UTF-8"?>
<xs:schema xmlns:xs="http://www.w3.org/2001/XMLSchema">
<xs:complexType name="dropListItem">
<xs:attribute name="id" type="xs:positiveInteger" use="required" />
<xs:attribute name="min" type="xs:nonNegativeInteger" />
<xs:attribute name="max" type="xs:positiveInteger" />
<xs:attribute name="chance" type="xs:decimal" />
</xs:complexType>
<xs:complexType name="dropList">
<xs:choice minOccurs="1" maxOccurs="unbounded">
<xs:element name="item" type="dropListItem" />
</xs:choice>
</xs:complexType>
<xs:element name="list">
<xs:complexType>
<xs:sequence>
@ -260,11 +249,81 @@
</xs:element>
<xs:element name="dropLists" minOccurs="0" maxOccurs="1">
<xs:complexType>
<xs:all>
<xs:element name="drop" type="dropList" minOccurs="0" maxOccurs="1" />
<xs:element name="spoil" type="dropList" minOccurs="0" maxOccurs="1" />
<xs:element name="lucky" type="dropList" minOccurs="0" maxOccurs="1" />
</xs:all>
<xs:sequence>
<xs:element name="drop" minOccurs="0">
<xs:complexType>
<xs:sequence>
<xs:element name="item" maxOccurs="unbounded" minOccurs="0">
<xs:complexType>
<xs:simpleContent>
<xs:extension base="xs:string">
<xs:attribute type="xs:int" name="id" use="required"/>
<xs:attribute type="xs:long" name="min" use="required"/>
<xs:attribute type="xs:long" name="max" use="required"/>
<xs:attribute type="xs:double" name="chance" use="required"/>
</xs:extension>
</xs:simpleContent>
</xs:complexType>
</xs:element>
<xs:element name="group" maxOccurs="unbounded" minOccurs="0">
<xs:complexType>
<xs:sequence>
<xs:element name="item" maxOccurs="unbounded" minOccurs="0">
<xs:complexType>
<xs:simpleContent>
<xs:extension base="xs:string">
<xs:attribute type="xs:int" name="id" use="required"/>
<xs:attribute type="xs:long" name="min" use="required"/>
<xs:attribute type="xs:long" name="max" use="required"/>
<xs:attribute type="xs:double" name="chance" use="required"/>
</xs:extension>
</xs:simpleContent>
</xs:complexType>
</xs:element>
</xs:sequence>
<xs:attribute type="xs:double" name="chance" use="optional"/>
</xs:complexType>
</xs:element>
</xs:sequence>
</xs:complexType>
</xs:element>
<xs:element name="spoil" minOccurs="0">
<xs:complexType>
<xs:sequence>
<xs:element name="item" maxOccurs="unbounded" minOccurs="0">
<xs:complexType>
<xs:simpleContent>
<xs:extension base="xs:string">
<xs:attribute type="xs:int" name="id" use="optional"/>
<xs:attribute type="xs:long" name="min" use="optional"/>
<xs:attribute type="xs:long" name="max" use="optional"/>
<xs:attribute type="xs:double" name="chance" use="optional"/>
</xs:extension>
</xs:simpleContent>
</xs:complexType>
</xs:element>
</xs:sequence>
</xs:complexType>
</xs:element>
<xs:element name="lucky" minOccurs="0">
<xs:complexType>
<xs:sequence>
<xs:element name="item" maxOccurs="unbounded" minOccurs="0">
<xs:complexType>
<xs:simpleContent>
<xs:extension base="xs:string">
<xs:attribute type="xs:int" name="id" use="optional"/>
<xs:attribute type="xs:long" name="min" use="optional"/>
<xs:attribute type="xs:long" name="max" use="optional"/>
<xs:attribute type="xs:double" name="chance" use="optional"/>
</xs:extension>
</xs:simpleContent>
</xs:complexType>
</xs:element>
</xs:sequence>
</xs:complexType>
</xs:element>
</xs:sequence>
</xs:complexType>
</xs:element>
<xs:element name="collision" minOccurs="0" maxOccurs="1">

View File

@ -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<Integer> clans = null;
Set<Integer> ignoreClanNpcIds = null;
List<DropHolder> dropLists = null;
List<DropGroupHolder> 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)

View File

@ -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<AISkillScope, List<Skill>> _aiSkillLists;
private Set<Integer> _clans;
private Set<Integer> _ignoreClanNpcIds;
private List<DropGroupHolder> _dropGroups;
private List<DropHolder> _dropListDeath;
private List<DropHolder> _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<DropGroupHolder> 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<DropGroupHolder> getDropGroups()
{
return _dropGroups;
}
public List<DropHolder> getDropList()
{
return _dropListDeath;
@ -685,11 +702,201 @@ public class NpcTemplate extends CreatureTemplate implements IIdentifiable
public List<ItemHolder> calculateDrops(DropType dropType, Creature victim, Creature killer)
{
final List<DropHolder> dropList = dropType == DropType.SPOIL ? _dropListSpoil : _dropListDeath;
if (dropList == null)
if (dropType == DropType.DROP)
{
return null;
// calculate group drops
List<ItemHolder> groupDrops = null;
if (_dropGroups != null)
{
groupDrops = calculateGroupDrops(victim, killer);
}
// calculate ungrouped drops
List<ItemHolder> 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<ItemHolder> 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<ItemHolder> calculatedDrops = null;
for (DropGroupHolder group : _dropGroups)
{
if (dropOccurrenceCounter <= 0)
{
break;
}
double groupRate = 1;
final List<DropHolder> 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<ItemHolder> calculateUngroupedDrops(DropType dropType, Creature victim, Creature killer)
{
final List<DropHolder> 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())
{

View File

@ -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 <http://www.gnu.org/licenses/>.
*/
package org.l2jmobius.gameserver.model.holders;
import java.util.ArrayList;
import java.util.List;
/**
* @author Mobius
*/
public class DropGroupHolder
{
private final List<DropHolder> _dropList = new ArrayList<>();
private final double _chance;
public DropGroupHolder(double chance)
{
_chance = chance;
}
public List<DropHolder> getDropList()
{
return _dropList;
}
public void addDrop(DropHolder holder)
{
_dropList.add(holder);
}
public double getChance()
{
return _chance;
}
}

View File

@ -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<DropGroupHolder> dropListGroups = npc.getTemplate().getDropGroups();
final List<DropHolder> dropListDeath = npc.getTemplate().getDropList();
final List<DropHolder> dropListSpoil = npc.getTemplate().getSpoilList();
if ((dropListDeath != null) || (dropListSpoil != null))
if ((dropListGroups != null) || (dropListDeath != null) || (dropListSpoil != null))
{
sb.append("<table width=275 cellpadding=0 cellspacing=0><tr>");
if (dropListDeath != null)
if ((dropListGroups != null) || (dropListDeath != null))
{
sb.append("<td align=center><button value=\"Show Drop\" width=100 height=25 action=\"bypass NpcViewMod dropList DROP " + npc.getObjectId() + "\" back=\"L2UI_CT1.Button_DF_Calculator_Down\" fore=\"L2UI_CT1.Button_DF_Calculator\"></td>");
}
@ -348,13 +350,40 @@ public class NpcViewMod implements IBypassHandler
private void sendNpcDropList(Player player, Npc npc, DropType dropType, int pageValue)
{
final List<DropHolder> templateList = dropType == DropType.SPOIL ? npc.getTemplate().getSpoilList() : npc.getTemplate().getDropList();
if (templateList == null)
List<DropHolder> dropList = null;
if (dropType == DropType.SPOIL)
{
dropList = new ArrayList<>(npc.getTemplate().getSpoilList());
}
else
{
final List<DropHolder> drops = npc.getTemplate().getDropList();
if (drops != null)
{
dropList = new ArrayList<>(drops);
}
final List<DropGroupHolder> 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<DropHolder> 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;

View File

@ -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())

View File

@ -1,16 +1,5 @@
<?xml version="1.0" encoding="UTF-8"?>
<xs:schema xmlns:xs="http://www.w3.org/2001/XMLSchema">
<xs:complexType name="dropListItem">
<xs:attribute name="id" type="xs:positiveInteger" use="required" />
<xs:attribute name="min" type="xs:nonNegativeInteger" />
<xs:attribute name="max" type="xs:positiveInteger" />
<xs:attribute name="chance" type="xs:decimal" />
</xs:complexType>
<xs:complexType name="dropList">
<xs:choice minOccurs="1" maxOccurs="unbounded">
<xs:element name="item" type="dropListItem" />
</xs:choice>
</xs:complexType>
<xs:element name="list">
<xs:complexType>
<xs:sequence>
@ -260,11 +249,81 @@
</xs:element>
<xs:element name="dropLists" minOccurs="0" maxOccurs="1">
<xs:complexType>
<xs:all>
<xs:element name="drop" type="dropList" minOccurs="0" maxOccurs="1" />
<xs:element name="spoil" type="dropList" minOccurs="0" maxOccurs="1" />
<xs:element name="lucky" type="dropList" minOccurs="0" maxOccurs="1" />
</xs:all>
<xs:sequence>
<xs:element name="drop" minOccurs="0">
<xs:complexType>
<xs:sequence>
<xs:element name="item" maxOccurs="unbounded" minOccurs="0">
<xs:complexType>
<xs:simpleContent>
<xs:extension base="xs:string">
<xs:attribute type="xs:int" name="id" use="required"/>
<xs:attribute type="xs:long" name="min" use="required"/>
<xs:attribute type="xs:long" name="max" use="required"/>
<xs:attribute type="xs:double" name="chance" use="required"/>
</xs:extension>
</xs:simpleContent>
</xs:complexType>
</xs:element>
<xs:element name="group" maxOccurs="unbounded" minOccurs="0">
<xs:complexType>
<xs:sequence>
<xs:element name="item" maxOccurs="unbounded" minOccurs="0">
<xs:complexType>
<xs:simpleContent>
<xs:extension base="xs:string">
<xs:attribute type="xs:int" name="id" use="required"/>
<xs:attribute type="xs:long" name="min" use="required"/>
<xs:attribute type="xs:long" name="max" use="required"/>
<xs:attribute type="xs:double" name="chance" use="required"/>
</xs:extension>
</xs:simpleContent>
</xs:complexType>
</xs:element>
</xs:sequence>
<xs:attribute type="xs:double" name="chance" use="optional"/>
</xs:complexType>
</xs:element>
</xs:sequence>
</xs:complexType>
</xs:element>
<xs:element name="spoil" minOccurs="0">
<xs:complexType>
<xs:sequence>
<xs:element name="item" maxOccurs="unbounded" minOccurs="0">
<xs:complexType>
<xs:simpleContent>
<xs:extension base="xs:string">
<xs:attribute type="xs:int" name="id" use="optional"/>
<xs:attribute type="xs:long" name="min" use="optional"/>
<xs:attribute type="xs:long" name="max" use="optional"/>
<xs:attribute type="xs:double" name="chance" use="optional"/>
</xs:extension>
</xs:simpleContent>
</xs:complexType>
</xs:element>
</xs:sequence>
</xs:complexType>
</xs:element>
<xs:element name="lucky" minOccurs="0">
<xs:complexType>
<xs:sequence>
<xs:element name="item" maxOccurs="unbounded" minOccurs="0">
<xs:complexType>
<xs:simpleContent>
<xs:extension base="xs:string">
<xs:attribute type="xs:int" name="id" use="optional"/>
<xs:attribute type="xs:long" name="min" use="optional"/>
<xs:attribute type="xs:long" name="max" use="optional"/>
<xs:attribute type="xs:double" name="chance" use="optional"/>
</xs:extension>
</xs:simpleContent>
</xs:complexType>
</xs:element>
</xs:sequence>
</xs:complexType>
</xs:element>
</xs:sequence>
</xs:complexType>
</xs:element>
<xs:element name="collision" minOccurs="0" maxOccurs="1">

View File

@ -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<Integer> clans = null;
Set<Integer> ignoreClanNpcIds = null;
List<DropHolder> dropLists = null;
List<DropGroupHolder> 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)

View File

@ -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<AISkillScope, List<Skill>> _aiSkillLists;
private Set<Integer> _clans;
private Set<Integer> _ignoreClanNpcIds;
private List<DropGroupHolder> _dropGroups;
private List<DropHolder> _dropListDeath;
private List<DropHolder> _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<DropGroupHolder> 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<DropGroupHolder> getDropGroups()
{
return _dropGroups;
}
public List<DropHolder> getDropList()
{
return _dropListDeath;
@ -685,11 +702,201 @@ public class NpcTemplate extends CreatureTemplate implements IIdentifiable
public List<ItemHolder> calculateDrops(DropType dropType, Creature victim, Creature killer)
{
final List<DropHolder> dropList = dropType == DropType.SPOIL ? _dropListSpoil : _dropListDeath;
if (dropList == null)
if (dropType == DropType.DROP)
{
return null;
// calculate group drops
List<ItemHolder> groupDrops = null;
if (_dropGroups != null)
{
groupDrops = calculateGroupDrops(victim, killer);
}
// calculate ungrouped drops
List<ItemHolder> 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<ItemHolder> 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<ItemHolder> calculatedDrops = null;
for (DropGroupHolder group : _dropGroups)
{
if (dropOccurrenceCounter <= 0)
{
break;
}
double groupRate = 1;
final List<DropHolder> 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<ItemHolder> calculateUngroupedDrops(DropType dropType, Creature victim, Creature killer)
{
final List<DropHolder> 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())
{

View File

@ -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 <http://www.gnu.org/licenses/>.
*/
package org.l2jmobius.gameserver.model.holders;
import java.util.ArrayList;
import java.util.List;
/**
* @author Mobius
*/
public class DropGroupHolder
{
private final List<DropHolder> _dropList = new ArrayList<>();
private final double _chance;
public DropGroupHolder(double chance)
{
_chance = chance;
}
public List<DropHolder> getDropList()
{
return _dropList;
}
public void addDrop(DropHolder holder)
{
_dropList.add(holder);
}
public double getChance()
{
return _chance;
}
}

View File

@ -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<DropGroupHolder> dropListGroups = npc.getTemplate().getDropGroups();
final List<DropHolder> dropListDeath = npc.getTemplate().getDropList();
final List<DropHolder> dropListSpoil = npc.getTemplate().getSpoilList();
if ((dropListDeath != null) || (dropListSpoil != null))
if ((dropListGroups != null) || (dropListDeath != null) || (dropListSpoil != null))
{
sb.append("<table width=275 cellpadding=0 cellspacing=0><tr>");
if (dropListDeath != null)
if ((dropListGroups != null) || (dropListDeath != null))
{
sb.append("<td align=center><button value=\"Show Drop\" width=100 height=25 action=\"bypass NpcViewMod dropList DROP " + npc.getObjectId() + "\" back=\"L2UI_CT1.Button_DF_Calculator_Down\" fore=\"L2UI_CT1.Button_DF_Calculator\"></td>");
}
@ -348,13 +350,40 @@ public class NpcViewMod implements IBypassHandler
private void sendNpcDropList(Player player, Npc npc, DropType dropType, int pageValue)
{
final List<DropHolder> templateList = dropType == DropType.SPOIL ? npc.getTemplate().getSpoilList() : npc.getTemplate().getDropList();
if (templateList == null)
List<DropHolder> dropList = null;
if (dropType == DropType.SPOIL)
{
dropList = new ArrayList<>(npc.getTemplate().getSpoilList());
}
else
{
final List<DropHolder> drops = npc.getTemplate().getDropList();
if (drops != null)
{
dropList = new ArrayList<>(drops);
}
final List<DropGroupHolder> 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<DropHolder> 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;

View File

@ -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())

View File

@ -1,16 +1,5 @@
<?xml version="1.0" encoding="UTF-8"?>
<xs:schema xmlns:xs="http://www.w3.org/2001/XMLSchema">
<xs:complexType name="dropListItem">
<xs:attribute name="id" type="xs:positiveInteger" use="required" />
<xs:attribute name="min" type="xs:nonNegativeInteger" />
<xs:attribute name="max" type="xs:positiveInteger" />
<xs:attribute name="chance" type="xs:decimal" />
</xs:complexType>
<xs:complexType name="dropList">
<xs:choice minOccurs="1" maxOccurs="unbounded">
<xs:element name="item" type="dropListItem" />
</xs:choice>
</xs:complexType>
<xs:element name="list">
<xs:complexType>
<xs:sequence>
@ -260,11 +249,81 @@
</xs:element>
<xs:element name="dropLists" minOccurs="0" maxOccurs="1">
<xs:complexType>
<xs:all>
<xs:element name="drop" type="dropList" minOccurs="0" maxOccurs="1" />
<xs:element name="spoil" type="dropList" minOccurs="0" maxOccurs="1" />
<xs:element name="lucky" type="dropList" minOccurs="0" maxOccurs="1" />
</xs:all>
<xs:sequence>
<xs:element name="drop" minOccurs="0">
<xs:complexType>
<xs:sequence>
<xs:element name="item" maxOccurs="unbounded" minOccurs="0">
<xs:complexType>
<xs:simpleContent>
<xs:extension base="xs:string">
<xs:attribute type="xs:int" name="id" use="required"/>
<xs:attribute type="xs:long" name="min" use="required"/>
<xs:attribute type="xs:long" name="max" use="required"/>
<xs:attribute type="xs:double" name="chance" use="required"/>
</xs:extension>
</xs:simpleContent>
</xs:complexType>
</xs:element>
<xs:element name="group" maxOccurs="unbounded" minOccurs="0">
<xs:complexType>
<xs:sequence>
<xs:element name="item" maxOccurs="unbounded" minOccurs="0">
<xs:complexType>
<xs:simpleContent>
<xs:extension base="xs:string">
<xs:attribute type="xs:int" name="id" use="required"/>
<xs:attribute type="xs:long" name="min" use="required"/>
<xs:attribute type="xs:long" name="max" use="required"/>
<xs:attribute type="xs:double" name="chance" use="required"/>
</xs:extension>
</xs:simpleContent>
</xs:complexType>
</xs:element>
</xs:sequence>
<xs:attribute type="xs:double" name="chance" use="optional"/>
</xs:complexType>
</xs:element>
</xs:sequence>
</xs:complexType>
</xs:element>
<xs:element name="spoil" minOccurs="0">
<xs:complexType>
<xs:sequence>
<xs:element name="item" maxOccurs="unbounded" minOccurs="0">
<xs:complexType>
<xs:simpleContent>
<xs:extension base="xs:string">
<xs:attribute type="xs:int" name="id" use="optional"/>
<xs:attribute type="xs:long" name="min" use="optional"/>
<xs:attribute type="xs:long" name="max" use="optional"/>
<xs:attribute type="xs:double" name="chance" use="optional"/>
</xs:extension>
</xs:simpleContent>
</xs:complexType>
</xs:element>
</xs:sequence>
</xs:complexType>
</xs:element>
<xs:element name="lucky" minOccurs="0">
<xs:complexType>
<xs:sequence>
<xs:element name="item" maxOccurs="unbounded" minOccurs="0">
<xs:complexType>
<xs:simpleContent>
<xs:extension base="xs:string">
<xs:attribute type="xs:int" name="id" use="optional"/>
<xs:attribute type="xs:long" name="min" use="optional"/>
<xs:attribute type="xs:long" name="max" use="optional"/>
<xs:attribute type="xs:double" name="chance" use="optional"/>
</xs:extension>
</xs:simpleContent>
</xs:complexType>
</xs:element>
</xs:sequence>
</xs:complexType>
</xs:element>
</xs:sequence>
</xs:complexType>
</xs:element>
<xs:element name="collision" minOccurs="0" maxOccurs="1">

View File

@ -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<Integer> clans = null;
Set<Integer> ignoreClanNpcIds = null;
List<DropHolder> dropLists = null;
List<DropGroupHolder> 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)

View File

@ -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<AISkillScope, List<Skill>> _aiSkillLists;
private Set<Integer> _clans;
private Set<Integer> _ignoreClanNpcIds;
private List<DropGroupHolder> _dropGroups;
private List<DropHolder> _dropListDeath;
private List<DropHolder> _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<DropGroupHolder> 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<DropGroupHolder> getDropGroups()
{
return _dropGroups;
}
public List<DropHolder> getDropList()
{
return _dropListDeath;
@ -685,11 +702,201 @@ public class NpcTemplate extends CreatureTemplate implements IIdentifiable
public List<ItemHolder> calculateDrops(DropType dropType, Creature victim, Creature killer)
{
final List<DropHolder> dropList = dropType == DropType.SPOIL ? _dropListSpoil : _dropListDeath;
if (dropList == null)
if (dropType == DropType.DROP)
{
return null;
// calculate group drops
List<ItemHolder> groupDrops = null;
if (_dropGroups != null)
{
groupDrops = calculateGroupDrops(victim, killer);
}
// calculate ungrouped drops
List<ItemHolder> 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<ItemHolder> 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<ItemHolder> calculatedDrops = null;
for (DropGroupHolder group : _dropGroups)
{
if (dropOccurrenceCounter <= 0)
{
break;
}
double groupRate = 1;
final List<DropHolder> 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<ItemHolder> calculateUngroupedDrops(DropType dropType, Creature victim, Creature killer)
{
final List<DropHolder> 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())
{

View File

@ -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 <http://www.gnu.org/licenses/>.
*/
package org.l2jmobius.gameserver.model.holders;
import java.util.ArrayList;
import java.util.List;
/**
* @author Mobius
*/
public class DropGroupHolder
{
private final List<DropHolder> _dropList = new ArrayList<>();
private final double _chance;
public DropGroupHolder(double chance)
{
_chance = chance;
}
public List<DropHolder> getDropList()
{
return _dropList;
}
public void addDrop(DropHolder holder)
{
_dropList.add(holder);
}
public double getChance()
{
return _chance;
}
}

View File

@ -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<DropGroupHolder> dropListGroups = npc.getTemplate().getDropGroups();
final List<DropHolder> dropListDeath = npc.getTemplate().getDropList();
final List<DropHolder> dropListSpoil = npc.getTemplate().getSpoilList();
if ((dropListDeath != null) || (dropListSpoil != null))
if ((dropListGroups != null) || (dropListDeath != null) || (dropListSpoil != null))
{
sb.append("<table width=275 cellpadding=0 cellspacing=0><tr>");
if (dropListDeath != null)
if ((dropListGroups != null) || (dropListDeath != null))
{
sb.append("<td align=center><button value=\"Show Drop\" width=100 height=25 action=\"bypass NpcViewMod dropList DROP " + npc.getObjectId() + "\" back=\"L2UI_CT1.Button_DF_Calculator_Down\" fore=\"L2UI_CT1.Button_DF_Calculator\"></td>");
}
@ -348,13 +350,40 @@ public class NpcViewMod implements IBypassHandler
private void sendNpcDropList(Player player, Npc npc, DropType dropType, int pageValue)
{
final List<DropHolder> templateList = dropType == DropType.SPOIL ? npc.getTemplate().getSpoilList() : npc.getTemplate().getDropList();
if (templateList == null)
List<DropHolder> dropList = null;
if (dropType == DropType.SPOIL)
{
dropList = new ArrayList<>(npc.getTemplate().getSpoilList());
}
else
{
final List<DropHolder> drops = npc.getTemplate().getDropList();
if (drops != null)
{
dropList = new ArrayList<>(drops);
}
final List<DropGroupHolder> 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<DropHolder> 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;

View File

@ -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())

View File

@ -1,16 +1,5 @@
<?xml version="1.0" encoding="UTF-8"?>
<xs:schema xmlns:xs="http://www.w3.org/2001/XMLSchema">
<xs:complexType name="dropListItem">
<xs:attribute name="id" type="xs:positiveInteger" use="required" />
<xs:attribute name="min" type="xs:nonNegativeInteger" />
<xs:attribute name="max" type="xs:positiveInteger" />
<xs:attribute name="chance" type="xs:decimal" />
</xs:complexType>
<xs:complexType name="dropList">
<xs:choice minOccurs="1" maxOccurs="unbounded">
<xs:element name="item" type="dropListItem" />
</xs:choice>
</xs:complexType>
<xs:element name="list">
<xs:complexType>
<xs:sequence>
@ -260,11 +249,81 @@
</xs:element>
<xs:element name="dropLists" minOccurs="0" maxOccurs="1">
<xs:complexType>
<xs:all>
<xs:element name="drop" type="dropList" minOccurs="0" maxOccurs="1" />
<xs:element name="spoil" type="dropList" minOccurs="0" maxOccurs="1" />
<xs:element name="lucky" type="dropList" minOccurs="0" maxOccurs="1" />
</xs:all>
<xs:sequence>
<xs:element name="drop" minOccurs="0">
<xs:complexType>
<xs:sequence>
<xs:element name="item" maxOccurs="unbounded" minOccurs="0">
<xs:complexType>
<xs:simpleContent>
<xs:extension base="xs:string">
<xs:attribute type="xs:int" name="id" use="required"/>
<xs:attribute type="xs:long" name="min" use="required"/>
<xs:attribute type="xs:long" name="max" use="required"/>
<xs:attribute type="xs:double" name="chance" use="required"/>
</xs:extension>
</xs:simpleContent>
</xs:complexType>
</xs:element>
<xs:element name="group" maxOccurs="unbounded" minOccurs="0">
<xs:complexType>
<xs:sequence>
<xs:element name="item" maxOccurs="unbounded" minOccurs="0">
<xs:complexType>
<xs:simpleContent>
<xs:extension base="xs:string">
<xs:attribute type="xs:int" name="id" use="required"/>
<xs:attribute type="xs:long" name="min" use="required"/>
<xs:attribute type="xs:long" name="max" use="required"/>
<xs:attribute type="xs:double" name="chance" use="required"/>
</xs:extension>
</xs:simpleContent>
</xs:complexType>
</xs:element>
</xs:sequence>
<xs:attribute type="xs:double" name="chance" use="optional"/>
</xs:complexType>
</xs:element>
</xs:sequence>
</xs:complexType>
</xs:element>
<xs:element name="spoil" minOccurs="0">
<xs:complexType>
<xs:sequence>
<xs:element name="item" maxOccurs="unbounded" minOccurs="0">
<xs:complexType>
<xs:simpleContent>
<xs:extension base="xs:string">
<xs:attribute type="xs:int" name="id" use="optional"/>
<xs:attribute type="xs:long" name="min" use="optional"/>
<xs:attribute type="xs:long" name="max" use="optional"/>
<xs:attribute type="xs:double" name="chance" use="optional"/>
</xs:extension>
</xs:simpleContent>
</xs:complexType>
</xs:element>
</xs:sequence>
</xs:complexType>
</xs:element>
<xs:element name="lucky" minOccurs="0">
<xs:complexType>
<xs:sequence>
<xs:element name="item" maxOccurs="unbounded" minOccurs="0">
<xs:complexType>
<xs:simpleContent>
<xs:extension base="xs:string">
<xs:attribute type="xs:int" name="id" use="optional"/>
<xs:attribute type="xs:long" name="min" use="optional"/>
<xs:attribute type="xs:long" name="max" use="optional"/>
<xs:attribute type="xs:double" name="chance" use="optional"/>
</xs:extension>
</xs:simpleContent>
</xs:complexType>
</xs:element>
</xs:sequence>
</xs:complexType>
</xs:element>
</xs:sequence>
</xs:complexType>
</xs:element>
<xs:element name="collision" minOccurs="0" maxOccurs="1">

View File

@ -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<Integer> clans = null;
Set<Integer> ignoreClanNpcIds = null;
List<DropHolder> dropLists = null;
List<DropGroupHolder> 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)

View File

@ -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<AISkillScope, List<Skill>> _aiSkillLists;
private Set<Integer> _clans;
private Set<Integer> _ignoreClanNpcIds;
private List<DropGroupHolder> _dropGroups;
private List<DropHolder> _dropListDeath;
private List<DropHolder> _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<DropGroupHolder> 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<DropGroupHolder> getDropGroups()
{
return _dropGroups;
}
public List<DropHolder> getDropList()
{
return _dropListDeath;
@ -685,11 +702,201 @@ public class NpcTemplate extends CreatureTemplate implements IIdentifiable
public List<ItemHolder> calculateDrops(DropType dropType, Creature victim, Creature killer)
{
final List<DropHolder> dropList = dropType == DropType.SPOIL ? _dropListSpoil : _dropListDeath;
if (dropList == null)
if (dropType == DropType.DROP)
{
return null;
// calculate group drops
List<ItemHolder> groupDrops = null;
if (_dropGroups != null)
{
groupDrops = calculateGroupDrops(victim, killer);
}
// calculate ungrouped drops
List<ItemHolder> 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<ItemHolder> 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<ItemHolder> calculatedDrops = null;
for (DropGroupHolder group : _dropGroups)
{
if (dropOccurrenceCounter <= 0)
{
break;
}
double groupRate = 1;
final List<DropHolder> 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<ItemHolder> calculateUngroupedDrops(DropType dropType, Creature victim, Creature killer)
{
final List<DropHolder> 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())
{

View File

@ -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 <http://www.gnu.org/licenses/>.
*/
package org.l2jmobius.gameserver.model.holders;
import java.util.ArrayList;
import java.util.List;
/**
* @author Mobius
*/
public class DropGroupHolder
{
private final List<DropHolder> _dropList = new ArrayList<>();
private final double _chance;
public DropGroupHolder(double chance)
{
_chance = chance;
}
public List<DropHolder> getDropList()
{
return _dropList;
}
public void addDrop(DropHolder holder)
{
_dropList.add(holder);
}
public double getChance()
{
return _chance;
}
}

View File

@ -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<DropGroupHolder> dropListGroups = npc.getTemplate().getDropGroups();
final List<DropHolder> dropListDeath = npc.getTemplate().getDropList();
final List<DropHolder> dropListSpoil = npc.getTemplate().getSpoilList();
if ((dropListDeath != null) || (dropListSpoil != null))
if ((dropListGroups != null) || (dropListDeath != null) || (dropListSpoil != null))
{
sb.append("<table width=275 cellpadding=0 cellspacing=0><tr>");
if (dropListDeath != null)
if ((dropListGroups != null) || (dropListDeath != null))
{
sb.append("<td align=center><button value=\"Show Drop\" width=100 height=25 action=\"bypass NpcViewMod dropList DROP " + npc.getObjectId() + "\" back=\"L2UI_CT1.Button_DF_Calculator_Down\" fore=\"L2UI_CT1.Button_DF_Calculator\"></td>");
}
@ -348,13 +350,40 @@ public class NpcViewMod implements IBypassHandler
private void sendNpcDropList(Player player, Npc npc, DropType dropType, int pageValue)
{
final List<DropHolder> templateList = dropType == DropType.SPOIL ? npc.getTemplate().getSpoilList() : npc.getTemplate().getDropList();
if (templateList == null)
List<DropHolder> dropList = null;
if (dropType == DropType.SPOIL)
{
dropList = new ArrayList<>(npc.getTemplate().getSpoilList());
}
else
{
final List<DropHolder> drops = npc.getTemplate().getDropList();
if (drops != null)
{
dropList = new ArrayList<>(drops);
}
final List<DropGroupHolder> 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<DropHolder> 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;

View File

@ -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())

View File

@ -1,16 +1,5 @@
<?xml version="1.0" encoding="UTF-8"?>
<xs:schema xmlns:xs="http://www.w3.org/2001/XMLSchema">
<xs:complexType name="dropListItem">
<xs:attribute name="id" type="xs:positiveInteger" use="required" />
<xs:attribute name="min" type="xs:nonNegativeInteger" />
<xs:attribute name="max" type="xs:positiveInteger" />
<xs:attribute name="chance" type="xs:decimal" />
</xs:complexType>
<xs:complexType name="dropList">
<xs:choice minOccurs="1" maxOccurs="unbounded">
<xs:element name="item" type="dropListItem" />
</xs:choice>
</xs:complexType>
<xs:element name="list">
<xs:complexType>
<xs:sequence>
@ -260,11 +249,81 @@
</xs:element>
<xs:element name="dropLists" minOccurs="0" maxOccurs="1">
<xs:complexType>
<xs:all>
<xs:element name="drop" type="dropList" minOccurs="0" maxOccurs="1" />
<xs:element name="spoil" type="dropList" minOccurs="0" maxOccurs="1" />
<xs:element name="lucky" type="dropList" minOccurs="0" maxOccurs="1" />
</xs:all>
<xs:sequence>
<xs:element name="drop" minOccurs="0">
<xs:complexType>
<xs:sequence>
<xs:element name="item" maxOccurs="unbounded" minOccurs="0">
<xs:complexType>
<xs:simpleContent>
<xs:extension base="xs:string">
<xs:attribute type="xs:int" name="id" use="required"/>
<xs:attribute type="xs:long" name="min" use="required"/>
<xs:attribute type="xs:long" name="max" use="required"/>
<xs:attribute type="xs:double" name="chance" use="required"/>
</xs:extension>
</xs:simpleContent>
</xs:complexType>
</xs:element>
<xs:element name="group" maxOccurs="unbounded" minOccurs="0">
<xs:complexType>
<xs:sequence>
<xs:element name="item" maxOccurs="unbounded" minOccurs="0">
<xs:complexType>
<xs:simpleContent>
<xs:extension base="xs:string">
<xs:attribute type="xs:int" name="id" use="required"/>
<xs:attribute type="xs:long" name="min" use="required"/>
<xs:attribute type="xs:long" name="max" use="required"/>
<xs:attribute type="xs:double" name="chance" use="required"/>
</xs:extension>
</xs:simpleContent>
</xs:complexType>
</xs:element>
</xs:sequence>
<xs:attribute type="xs:double" name="chance" use="optional"/>
</xs:complexType>
</xs:element>
</xs:sequence>
</xs:complexType>
</xs:element>
<xs:element name="spoil" minOccurs="0">
<xs:complexType>
<xs:sequence>
<xs:element name="item" maxOccurs="unbounded" minOccurs="0">
<xs:complexType>
<xs:simpleContent>
<xs:extension base="xs:string">
<xs:attribute type="xs:int" name="id" use="optional"/>
<xs:attribute type="xs:long" name="min" use="optional"/>
<xs:attribute type="xs:long" name="max" use="optional"/>
<xs:attribute type="xs:double" name="chance" use="optional"/>
</xs:extension>
</xs:simpleContent>
</xs:complexType>
</xs:element>
</xs:sequence>
</xs:complexType>
</xs:element>
<xs:element name="lucky" minOccurs="0">
<xs:complexType>
<xs:sequence>
<xs:element name="item" maxOccurs="unbounded" minOccurs="0">
<xs:complexType>
<xs:simpleContent>
<xs:extension base="xs:string">
<xs:attribute type="xs:int" name="id" use="optional"/>
<xs:attribute type="xs:long" name="min" use="optional"/>
<xs:attribute type="xs:long" name="max" use="optional"/>
<xs:attribute type="xs:double" name="chance" use="optional"/>
</xs:extension>
</xs:simpleContent>
</xs:complexType>
</xs:element>
</xs:sequence>
</xs:complexType>
</xs:element>
</xs:sequence>
</xs:complexType>
</xs:element>
<xs:element name="collision" minOccurs="0" maxOccurs="1">

View File

@ -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<Integer> clans = null;
Set<Integer> ignoreClanNpcIds = null;
List<DropHolder> dropLists = null;
List<DropGroupHolder> 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)

View File

@ -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<AISkillScope, List<Skill>> _aiSkillLists;
private Set<Integer> _clans;
private Set<Integer> _ignoreClanNpcIds;
private List<DropGroupHolder> _dropGroups;
private List<DropHolder> _dropListDeath;
private List<DropHolder> _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<DropGroupHolder> 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<DropGroupHolder> getDropGroups()
{
return _dropGroups;
}
public List<DropHolder> getDropList()
{
return _dropListDeath;
@ -685,11 +702,201 @@ public class NpcTemplate extends CreatureTemplate implements IIdentifiable
public List<ItemHolder> calculateDrops(DropType dropType, Creature victim, Creature killer)
{
final List<DropHolder> dropList = dropType == DropType.SPOIL ? _dropListSpoil : _dropListDeath;
if (dropList == null)
if (dropType == DropType.DROP)
{
return null;
// calculate group drops
List<ItemHolder> groupDrops = null;
if (_dropGroups != null)
{
groupDrops = calculateGroupDrops(victim, killer);
}
// calculate ungrouped drops
List<ItemHolder> 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<ItemHolder> 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<ItemHolder> calculatedDrops = null;
for (DropGroupHolder group : _dropGroups)
{
if (dropOccurrenceCounter <= 0)
{
break;
}
double groupRate = 1;
final List<DropHolder> 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<ItemHolder> calculateUngroupedDrops(DropType dropType, Creature victim, Creature killer)
{
final List<DropHolder> 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())
{

View File

@ -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 <http://www.gnu.org/licenses/>.
*/
package org.l2jmobius.gameserver.model.holders;
import java.util.ArrayList;
import java.util.List;
/**
* @author Mobius
*/
public class DropGroupHolder
{
private final List<DropHolder> _dropList = new ArrayList<>();
private final double _chance;
public DropGroupHolder(double chance)
{
_chance = chance;
}
public List<DropHolder> getDropList()
{
return _dropList;
}
public void addDrop(DropHolder holder)
{
_dropList.add(holder);
}
public double getChance()
{
return _chance;
}
}

View File

@ -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<DropGroupHolder> dropListGroups = npc.getTemplate().getDropGroups();
final List<DropHolder> dropListDeath = npc.getTemplate().getDropList();
final List<DropHolder> dropListSpoil = npc.getTemplate().getSpoilList();
if ((dropListDeath != null) || (dropListSpoil != null))
if ((dropListGroups != null) || (dropListDeath != null) || (dropListSpoil != null))
{
sb.append("<table width=275 cellpadding=0 cellspacing=0><tr>");
if (dropListDeath != null)
if ((dropListGroups != null) || (dropListDeath != null))
{
sb.append("<td align=center><button value=\"Show Drop\" width=100 height=25 action=\"bypass NpcViewMod dropList DROP " + npc.getObjectId() + "\" back=\"L2UI_CT1.Button_DF_Calculator_Down\" fore=\"L2UI_CT1.Button_DF_Calculator\"></td>");
}
@ -348,13 +350,40 @@ public class NpcViewMod implements IBypassHandler
private void sendNpcDropList(Player player, Npc npc, DropType dropType, int pageValue)
{
final List<DropHolder> templateList = dropType == DropType.SPOIL ? npc.getTemplate().getSpoilList() : npc.getTemplate().getDropList();
if (templateList == null)
List<DropHolder> dropList = null;
if (dropType == DropType.SPOIL)
{
dropList = new ArrayList<>(npc.getTemplate().getSpoilList());
}
else
{
final List<DropHolder> drops = npc.getTemplate().getDropList();
if (drops != null)
{
dropList = new ArrayList<>(drops);
}
final List<DropGroupHolder> 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<DropHolder> 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;

View File

@ -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())

View File

@ -1,16 +1,5 @@
<?xml version="1.0" encoding="UTF-8"?>
<xs:schema xmlns:xs="http://www.w3.org/2001/XMLSchema">
<xs:complexType name="dropListItem">
<xs:attribute name="id" type="xs:positiveInteger" use="required" />
<xs:attribute name="min" type="xs:nonNegativeInteger" />
<xs:attribute name="max" type="xs:positiveInteger" />
<xs:attribute name="chance" type="xs:decimal" />
</xs:complexType>
<xs:complexType name="dropList">
<xs:choice minOccurs="1" maxOccurs="unbounded">
<xs:element name="item" type="dropListItem" />
</xs:choice>
</xs:complexType>
<xs:element name="list">
<xs:complexType>
<xs:sequence>
@ -260,11 +249,81 @@
</xs:element>
<xs:element name="dropLists" minOccurs="0" maxOccurs="1">
<xs:complexType>
<xs:all>
<xs:element name="drop" type="dropList" minOccurs="0" maxOccurs="1" />
<xs:element name="spoil" type="dropList" minOccurs="0" maxOccurs="1" />
<xs:element name="lucky" type="dropList" minOccurs="0" maxOccurs="1" />
</xs:all>
<xs:sequence>
<xs:element name="drop" minOccurs="0">
<xs:complexType>
<xs:sequence>
<xs:element name="item" maxOccurs="unbounded" minOccurs="0">
<xs:complexType>
<xs:simpleContent>
<xs:extension base="xs:string">
<xs:attribute type="xs:int" name="id" use="required"/>
<xs:attribute type="xs:long" name="min" use="required"/>
<xs:attribute type="xs:long" name="max" use="required"/>
<xs:attribute type="xs:double" name="chance" use="required"/>
</xs:extension>
</xs:simpleContent>
</xs:complexType>
</xs:element>
<xs:element name="group" maxOccurs="unbounded" minOccurs="0">
<xs:complexType>
<xs:sequence>
<xs:element name="item" maxOccurs="unbounded" minOccurs="0">
<xs:complexType>
<xs:simpleContent>
<xs:extension base="xs:string">
<xs:attribute type="xs:int" name="id" use="required"/>
<xs:attribute type="xs:long" name="min" use="required"/>
<xs:attribute type="xs:long" name="max" use="required"/>
<xs:attribute type="xs:double" name="chance" use="required"/>
</xs:extension>
</xs:simpleContent>
</xs:complexType>
</xs:element>
</xs:sequence>
<xs:attribute type="xs:double" name="chance" use="optional"/>
</xs:complexType>
</xs:element>
</xs:sequence>
</xs:complexType>
</xs:element>
<xs:element name="spoil" minOccurs="0">
<xs:complexType>
<xs:sequence>
<xs:element name="item" maxOccurs="unbounded" minOccurs="0">
<xs:complexType>
<xs:simpleContent>
<xs:extension base="xs:string">
<xs:attribute type="xs:int" name="id" use="optional"/>
<xs:attribute type="xs:long" name="min" use="optional"/>
<xs:attribute type="xs:long" name="max" use="optional"/>
<xs:attribute type="xs:double" name="chance" use="optional"/>
</xs:extension>
</xs:simpleContent>
</xs:complexType>
</xs:element>
</xs:sequence>
</xs:complexType>
</xs:element>
<xs:element name="lucky" minOccurs="0">
<xs:complexType>
<xs:sequence>
<xs:element name="item" maxOccurs="unbounded" minOccurs="0">
<xs:complexType>
<xs:simpleContent>
<xs:extension base="xs:string">
<xs:attribute type="xs:int" name="id" use="optional"/>
<xs:attribute type="xs:long" name="min" use="optional"/>
<xs:attribute type="xs:long" name="max" use="optional"/>
<xs:attribute type="xs:double" name="chance" use="optional"/>
</xs:extension>
</xs:simpleContent>
</xs:complexType>
</xs:element>
</xs:sequence>
</xs:complexType>
</xs:element>
</xs:sequence>
</xs:complexType>
</xs:element>
<xs:element name="collision" minOccurs="0" maxOccurs="1">

View File

@ -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<Integer> clans = null;
Set<Integer> ignoreClanNpcIds = null;
List<DropHolder> dropLists = null;
List<DropGroupHolder> 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)

View File

@ -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<AISkillScope, List<Skill>> _aiSkillLists;
private Set<Integer> _clans;
private Set<Integer> _ignoreClanNpcIds;
private List<DropGroupHolder> _dropGroups;
private List<DropHolder> _dropListDeath;
private List<DropHolder> _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<DropGroupHolder> 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<DropGroupHolder> getDropGroups()
{
return _dropGroups;
}
public List<DropHolder> getDropList()
{
return _dropListDeath;
@ -685,11 +702,201 @@ public class NpcTemplate extends CreatureTemplate implements IIdentifiable
public List<ItemHolder> calculateDrops(DropType dropType, Creature victim, Creature killer)
{
final List<DropHolder> dropList = dropType == DropType.SPOIL ? _dropListSpoil : _dropListDeath;
if (dropList == null)
if (dropType == DropType.DROP)
{
return null;
// calculate group drops
List<ItemHolder> groupDrops = null;
if (_dropGroups != null)
{
groupDrops = calculateGroupDrops(victim, killer);
}
// calculate ungrouped drops
List<ItemHolder> 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<ItemHolder> 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<ItemHolder> calculatedDrops = null;
for (DropGroupHolder group : _dropGroups)
{
if (dropOccurrenceCounter <= 0)
{
break;
}
double groupRate = 1;
final List<DropHolder> 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<ItemHolder> calculateUngroupedDrops(DropType dropType, Creature victim, Creature killer)
{
final List<DropHolder> 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())
{

View File

@ -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 <http://www.gnu.org/licenses/>.
*/
package org.l2jmobius.gameserver.model.holders;
import java.util.ArrayList;
import java.util.List;
/**
* @author Mobius
*/
public class DropGroupHolder
{
private final List<DropHolder> _dropList = new ArrayList<>();
private final double _chance;
public DropGroupHolder(double chance)
{
_chance = chance;
}
public List<DropHolder> getDropList()
{
return _dropList;
}
public void addDrop(DropHolder holder)
{
_dropList.add(holder);
}
public double getChance()
{
return _chance;
}
}

View File

@ -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<DropGroupHolder> dropListGroups = npc.getTemplate().getDropGroups();
final List<DropHolder> dropListDeath = npc.getTemplate().getDropList();
final List<DropHolder> dropListSpoil = npc.getTemplate().getSpoilList();
if ((dropListDeath != null) || (dropListSpoil != null))
if ((dropListGroups != null) || (dropListDeath != null) || (dropListSpoil != null))
{
sb.append("<table width=275 cellpadding=0 cellspacing=0><tr>");
if (dropListDeath != null)
if ((dropListGroups != null) || (dropListDeath != null))
{
sb.append("<td align=center><button value=\"Show Drop\" width=100 height=25 action=\"bypass NpcViewMod dropList DROP " + npc.getObjectId() + "\" back=\"L2UI_CT1.Button_DF_Calculator_Down\" fore=\"L2UI_CT1.Button_DF_Calculator\"></td>");
}
@ -348,13 +350,40 @@ public class NpcViewMod implements IBypassHandler
private void sendNpcDropList(Player player, Npc npc, DropType dropType, int pageValue)
{
final List<DropHolder> templateList = dropType == DropType.SPOIL ? npc.getTemplate().getSpoilList() : npc.getTemplate().getDropList();
if (templateList == null)
List<DropHolder> dropList = null;
if (dropType == DropType.SPOIL)
{
dropList = new ArrayList<>(npc.getTemplate().getSpoilList());
}
else
{
final List<DropHolder> drops = npc.getTemplate().getDropList();
if (drops != null)
{
dropList = new ArrayList<>(drops);
}
final List<DropGroupHolder> 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<DropHolder> 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;

View File

@ -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())

View File

@ -1,16 +1,5 @@
<?xml version="1.0" encoding="UTF-8"?>
<xs:schema xmlns:xs="http://www.w3.org/2001/XMLSchema">
<xs:complexType name="dropListItem">
<xs:attribute name="id" type="xs:positiveInteger" use="required" />
<xs:attribute name="min" type="xs:nonNegativeInteger" />
<xs:attribute name="max" type="xs:positiveInteger" />
<xs:attribute name="chance" type="xs:decimal" />
</xs:complexType>
<xs:complexType name="dropList">
<xs:choice minOccurs="1" maxOccurs="unbounded">
<xs:element name="item" type="dropListItem" />
</xs:choice>
</xs:complexType>
<xs:element name="list">
<xs:complexType>
<xs:sequence>
@ -260,11 +249,81 @@
</xs:element>
<xs:element name="dropLists" minOccurs="0" maxOccurs="1">
<xs:complexType>
<xs:all>
<xs:element name="drop" type="dropList" minOccurs="0" maxOccurs="1" />
<xs:element name="spoil" type="dropList" minOccurs="0" maxOccurs="1" />
<xs:element name="lucky" type="dropList" minOccurs="0" maxOccurs="1" />
</xs:all>
<xs:sequence>
<xs:element name="drop" minOccurs="0">
<xs:complexType>
<xs:sequence>
<xs:element name="item" maxOccurs="unbounded" minOccurs="0">
<xs:complexType>
<xs:simpleContent>
<xs:extension base="xs:string">
<xs:attribute type="xs:int" name="id" use="required"/>
<xs:attribute type="xs:long" name="min" use="required"/>
<xs:attribute type="xs:long" name="max" use="required"/>
<xs:attribute type="xs:double" name="chance" use="required"/>
</xs:extension>
</xs:simpleContent>
</xs:complexType>
</xs:element>
<xs:element name="group" maxOccurs="unbounded" minOccurs="0">
<xs:complexType>
<xs:sequence>
<xs:element name="item" maxOccurs="unbounded" minOccurs="0">
<xs:complexType>
<xs:simpleContent>
<xs:extension base="xs:string">
<xs:attribute type="xs:int" name="id" use="required"/>
<xs:attribute type="xs:long" name="min" use="required"/>
<xs:attribute type="xs:long" name="max" use="required"/>
<xs:attribute type="xs:double" name="chance" use="required"/>
</xs:extension>
</xs:simpleContent>
</xs:complexType>
</xs:element>
</xs:sequence>
<xs:attribute type="xs:double" name="chance" use="optional"/>
</xs:complexType>
</xs:element>
</xs:sequence>
</xs:complexType>
</xs:element>
<xs:element name="spoil" minOccurs="0">
<xs:complexType>
<xs:sequence>
<xs:element name="item" maxOccurs="unbounded" minOccurs="0">
<xs:complexType>
<xs:simpleContent>
<xs:extension base="xs:string">
<xs:attribute type="xs:int" name="id" use="optional"/>
<xs:attribute type="xs:long" name="min" use="optional"/>
<xs:attribute type="xs:long" name="max" use="optional"/>
<xs:attribute type="xs:double" name="chance" use="optional"/>
</xs:extension>
</xs:simpleContent>
</xs:complexType>
</xs:element>
</xs:sequence>
</xs:complexType>
</xs:element>
<xs:element name="lucky" minOccurs="0">
<xs:complexType>
<xs:sequence>
<xs:element name="item" maxOccurs="unbounded" minOccurs="0">
<xs:complexType>
<xs:simpleContent>
<xs:extension base="xs:string">
<xs:attribute type="xs:int" name="id" use="optional"/>
<xs:attribute type="xs:long" name="min" use="optional"/>
<xs:attribute type="xs:long" name="max" use="optional"/>
<xs:attribute type="xs:double" name="chance" use="optional"/>
</xs:extension>
</xs:simpleContent>
</xs:complexType>
</xs:element>
</xs:sequence>
</xs:complexType>
</xs:element>
</xs:sequence>
</xs:complexType>
</xs:element>
<xs:element name="collision" minOccurs="0" maxOccurs="1">

View File

@ -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<Integer> clans = null;
Set<Integer> ignoreClanNpcIds = null;
List<DropHolder> dropLists = null;
List<DropGroupHolder> 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)

View File

@ -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<AISkillScope, List<Skill>> _aiSkillLists;
private Set<Integer> _clans;
private Set<Integer> _ignoreClanNpcIds;
private List<DropGroupHolder> _dropGroups;
private List<DropHolder> _dropListDeath;
private List<DropHolder> _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<DropGroupHolder> 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<DropGroupHolder> getDropGroups()
{
return _dropGroups;
}
public List<DropHolder> getDropList()
{
return _dropListDeath;
@ -685,11 +702,201 @@ public class NpcTemplate extends CreatureTemplate implements IIdentifiable
public List<ItemHolder> calculateDrops(DropType dropType, Creature victim, Creature killer)
{
final List<DropHolder> dropList = dropType == DropType.SPOIL ? _dropListSpoil : _dropListDeath;
if (dropList == null)
if (dropType == DropType.DROP)
{
return null;
// calculate group drops
List<ItemHolder> groupDrops = null;
if (_dropGroups != null)
{
groupDrops = calculateGroupDrops(victim, killer);
}
// calculate ungrouped drops
List<ItemHolder> 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<ItemHolder> 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<ItemHolder> calculatedDrops = null;
for (DropGroupHolder group : _dropGroups)
{
if (dropOccurrenceCounter <= 0)
{
break;
}
double groupRate = 1;
final List<DropHolder> 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<ItemHolder> calculateUngroupedDrops(DropType dropType, Creature victim, Creature killer)
{
final List<DropHolder> 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())
{

View File

@ -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 <http://www.gnu.org/licenses/>.
*/
package org.l2jmobius.gameserver.model.holders;
import java.util.ArrayList;
import java.util.List;
/**
* @author Mobius
*/
public class DropGroupHolder
{
private final List<DropHolder> _dropList = new ArrayList<>();
private final double _chance;
public DropGroupHolder(double chance)
{
_chance = chance;
}
public List<DropHolder> getDropList()
{
return _dropList;
}
public void addDrop(DropHolder holder)
{
_dropList.add(holder);
}
public double getChance()
{
return _chance;
}
}

View File

@ -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<DropGroupHolder> dropListGroups = npc.getTemplate().getDropGroups();
final List<DropHolder> dropListDeath = npc.getTemplate().getDropList();
final List<DropHolder> dropListSpoil = npc.getTemplate().getSpoilList();
if ((dropListDeath != null) || (dropListSpoil != null))
if ((dropListGroups != null) || (dropListDeath != null) || (dropListSpoil != null))
{
sb.append("<table width=275 cellpadding=0 cellspacing=0><tr>");
if (dropListDeath != null)
if ((dropListGroups != null) || (dropListDeath != null))
{
sb.append("<td align=center><button value=\"Show Drop\" width=100 height=25 action=\"bypass NpcViewMod dropList DROP " + npc.getObjectId() + "\" back=\"L2UI_CT1.Button_DF_Calculator_Down\" fore=\"L2UI_CT1.Button_DF_Calculator\"></td>");
}
@ -348,13 +350,40 @@ public class NpcViewMod implements IBypassHandler
private void sendNpcDropList(Player player, Npc npc, DropType dropType, int pageValue)
{
final List<DropHolder> templateList = dropType == DropType.SPOIL ? npc.getTemplate().getSpoilList() : npc.getTemplate().getDropList();
if (templateList == null)
List<DropHolder> dropList = null;
if (dropType == DropType.SPOIL)
{
dropList = new ArrayList<>(npc.getTemplate().getSpoilList());
}
else
{
final List<DropHolder> drops = npc.getTemplate().getDropList();
if (drops != null)
{
dropList = new ArrayList<>(drops);
}
final List<DropGroupHolder> 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<DropHolder> 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;

View File

@ -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())

View File

@ -1,16 +1,5 @@
<?xml version="1.0" encoding="UTF-8"?>
<xs:schema xmlns:xs="http://www.w3.org/2001/XMLSchema">
<xs:complexType name="dropListItem">
<xs:attribute name="id" type="xs:positiveInteger" use="required" />
<xs:attribute name="min" type="xs:nonNegativeInteger" />
<xs:attribute name="max" type="xs:positiveInteger" />
<xs:attribute name="chance" type="xs:decimal" />
</xs:complexType>
<xs:complexType name="dropList">
<xs:choice minOccurs="1" maxOccurs="unbounded">
<xs:element name="item" type="dropListItem" />
</xs:choice>
</xs:complexType>
<xs:element name="list">
<xs:complexType>
<xs:sequence>
@ -260,11 +249,81 @@
</xs:element>
<xs:element name="dropLists" minOccurs="0" maxOccurs="1">
<xs:complexType>
<xs:all>
<xs:element name="drop" type="dropList" minOccurs="0" maxOccurs="1" />
<xs:element name="spoil" type="dropList" minOccurs="0" maxOccurs="1" />
<xs:element name="lucky" type="dropList" minOccurs="0" maxOccurs="1" />
</xs:all>
<xs:sequence>
<xs:element name="drop" minOccurs="0">
<xs:complexType>
<xs:sequence>
<xs:element name="item" maxOccurs="unbounded" minOccurs="0">
<xs:complexType>
<xs:simpleContent>
<xs:extension base="xs:string">
<xs:attribute type="xs:int" name="id" use="required"/>
<xs:attribute type="xs:long" name="min" use="required"/>
<xs:attribute type="xs:long" name="max" use="required"/>
<xs:attribute type="xs:double" name="chance" use="required"/>
</xs:extension>
</xs:simpleContent>
</xs:complexType>
</xs:element>
<xs:element name="group" maxOccurs="unbounded" minOccurs="0">
<xs:complexType>
<xs:sequence>
<xs:element name="item" maxOccurs="unbounded" minOccurs="0">
<xs:complexType>
<xs:simpleContent>
<xs:extension base="xs:string">
<xs:attribute type="xs:int" name="id" use="required"/>
<xs:attribute type="xs:long" name="min" use="required"/>
<xs:attribute type="xs:long" name="max" use="required"/>
<xs:attribute type="xs:double" name="chance" use="required"/>
</xs:extension>
</xs:simpleContent>
</xs:complexType>
</xs:element>
</xs:sequence>
<xs:attribute type="xs:double" name="chance" use="optional"/>
</xs:complexType>
</xs:element>
</xs:sequence>
</xs:complexType>
</xs:element>
<xs:element name="spoil" minOccurs="0">
<xs:complexType>
<xs:sequence>
<xs:element name="item" maxOccurs="unbounded" minOccurs="0">
<xs:complexType>
<xs:simpleContent>
<xs:extension base="xs:string">
<xs:attribute type="xs:int" name="id" use="optional"/>
<xs:attribute type="xs:long" name="min" use="optional"/>
<xs:attribute type="xs:long" name="max" use="optional"/>
<xs:attribute type="xs:double" name="chance" use="optional"/>
</xs:extension>
</xs:simpleContent>
</xs:complexType>
</xs:element>
</xs:sequence>
</xs:complexType>
</xs:element>
<xs:element name="lucky" minOccurs="0">
<xs:complexType>
<xs:sequence>
<xs:element name="item" maxOccurs="unbounded" minOccurs="0">
<xs:complexType>
<xs:simpleContent>
<xs:extension base="xs:string">
<xs:attribute type="xs:int" name="id" use="optional"/>
<xs:attribute type="xs:long" name="min" use="optional"/>
<xs:attribute type="xs:long" name="max" use="optional"/>
<xs:attribute type="xs:double" name="chance" use="optional"/>
</xs:extension>
</xs:simpleContent>
</xs:complexType>
</xs:element>
</xs:sequence>
</xs:complexType>
</xs:element>
</xs:sequence>
</xs:complexType>
</xs:element>
<xs:element name="collision" minOccurs="0" maxOccurs="1">

View File

@ -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<Integer> clans = null;
Set<Integer> ignoreClanNpcIds = null;
List<DropHolder> dropLists = null;
List<DropGroupHolder> 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)

View File

@ -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<AISkillScope, List<Skill>> _aiSkillLists;
private Set<Integer> _clans;
private Set<Integer> _ignoreClanNpcIds;
private List<DropGroupHolder> _dropGroups;
private List<DropHolder> _dropListDeath;
private List<DropHolder> _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<DropGroupHolder> 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<DropGroupHolder> getDropGroups()
{
return _dropGroups;
}
public List<DropHolder> getDropList()
{
return _dropListDeath;
@ -685,11 +702,201 @@ public class NpcTemplate extends CreatureTemplate implements IIdentifiable
public List<ItemHolder> calculateDrops(DropType dropType, Creature victim, Creature killer)
{
final List<DropHolder> dropList = dropType == DropType.SPOIL ? _dropListSpoil : _dropListDeath;
if (dropList == null)
if (dropType == DropType.DROP)
{
return null;
// calculate group drops
List<ItemHolder> groupDrops = null;
if (_dropGroups != null)
{
groupDrops = calculateGroupDrops(victim, killer);
}
// calculate ungrouped drops
List<ItemHolder> 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<ItemHolder> 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<ItemHolder> calculatedDrops = null;
for (DropGroupHolder group : _dropGroups)
{
if (dropOccurrenceCounter <= 0)
{
break;
}
double groupRate = 1;
final List<DropHolder> 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<ItemHolder> calculateUngroupedDrops(DropType dropType, Creature victim, Creature killer)
{
final List<DropHolder> 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())
{

View File

@ -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 <http://www.gnu.org/licenses/>.
*/
package org.l2jmobius.gameserver.model.holders;
import java.util.ArrayList;
import java.util.List;
/**
* @author Mobius
*/
public class DropGroupHolder
{
private final List<DropHolder> _dropList = new ArrayList<>();
private final double _chance;
public DropGroupHolder(double chance)
{
_chance = chance;
}
public List<DropHolder> getDropList()
{
return _dropList;
}
public void addDrop(DropHolder holder)
{
_dropList.add(holder);
}
public double getChance()
{
return _chance;
}
}

View File

@ -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<DropGroupHolder> dropListGroups = npc.getTemplate().getDropGroups();
final List<DropHolder> dropListDeath = npc.getTemplate().getDropList();
final List<DropHolder> dropListSpoil = npc.getTemplate().getSpoilList();
if ((dropListDeath != null) || (dropListSpoil != null))
if ((dropListGroups != null) || (dropListDeath != null) || (dropListSpoil != null))
{
sb.append("<table width=275 cellpadding=0 cellspacing=0><tr>");
if (dropListDeath != null)
if ((dropListGroups != null) || (dropListDeath != null))
{
sb.append("<td align=center><button value=\"Show Drop\" width=100 height=25 action=\"bypass NpcViewMod dropList DROP " + npc.getObjectId() + "\" back=\"L2UI_CT1.Button_DF_Calculator_Down\" fore=\"L2UI_CT1.Button_DF_Calculator\"></td>");
}
@ -348,13 +350,40 @@ public class NpcViewMod implements IBypassHandler
private void sendNpcDropList(Player player, Npc npc, DropType dropType, int pageValue)
{
final List<DropHolder> templateList = dropType == DropType.SPOIL ? npc.getTemplate().getSpoilList() : npc.getTemplate().getDropList();
if (templateList == null)
List<DropHolder> dropList = null;
if (dropType == DropType.SPOIL)
{
dropList = new ArrayList<>(npc.getTemplate().getSpoilList());
}
else
{
final List<DropHolder> drops = npc.getTemplate().getDropList();
if (drops != null)
{
dropList = new ArrayList<>(drops);
}
final List<DropGroupHolder> 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<DropHolder> 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;

View File

@ -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())

View File

@ -1,16 +1,5 @@
<?xml version="1.0" encoding="UTF-8"?>
<xs:schema xmlns:xs="http://www.w3.org/2001/XMLSchema">
<xs:complexType name="dropListItem">
<xs:attribute name="id" type="xs:positiveInteger" use="required" />
<xs:attribute name="min" type="xs:nonNegativeInteger" />
<xs:attribute name="max" type="xs:positiveInteger" />
<xs:attribute name="chance" type="xs:decimal" />
</xs:complexType>
<xs:complexType name="dropList">
<xs:choice minOccurs="1" maxOccurs="unbounded">
<xs:element name="item" type="dropListItem" />
</xs:choice>
</xs:complexType>
<xs:element name="list">
<xs:complexType>
<xs:sequence>
@ -260,11 +249,81 @@
</xs:element>
<xs:element name="dropLists" minOccurs="0" maxOccurs="1">
<xs:complexType>
<xs:all>
<xs:element name="drop" type="dropList" minOccurs="0" maxOccurs="1" />
<xs:element name="spoil" type="dropList" minOccurs="0" maxOccurs="1" />
<xs:element name="lucky" type="dropList" minOccurs="0" maxOccurs="1" />
</xs:all>
<xs:sequence>
<xs:element name="drop" minOccurs="0">
<xs:complexType>
<xs:sequence>
<xs:element name="item" maxOccurs="unbounded" minOccurs="0">
<xs:complexType>
<xs:simpleContent>
<xs:extension base="xs:string">
<xs:attribute type="xs:int" name="id" use="required"/>
<xs:attribute type="xs:long" name="min" use="required"/>
<xs:attribute type="xs:long" name="max" use="required"/>
<xs:attribute type="xs:double" name="chance" use="required"/>
</xs:extension>
</xs:simpleContent>
</xs:complexType>
</xs:element>
<xs:element name="group" maxOccurs="unbounded" minOccurs="0">
<xs:complexType>
<xs:sequence>
<xs:element name="item" maxOccurs="unbounded" minOccurs="0">
<xs:complexType>
<xs:simpleContent>
<xs:extension base="xs:string">
<xs:attribute type="xs:int" name="id" use="required"/>
<xs:attribute type="xs:long" name="min" use="required"/>
<xs:attribute type="xs:long" name="max" use="required"/>
<xs:attribute type="xs:double" name="chance" use="required"/>
</xs:extension>
</xs:simpleContent>
</xs:complexType>
</xs:element>
</xs:sequence>
<xs:attribute type="xs:double" name="chance" use="optional"/>
</xs:complexType>
</xs:element>
</xs:sequence>
</xs:complexType>
</xs:element>
<xs:element name="spoil" minOccurs="0">
<xs:complexType>
<xs:sequence>
<xs:element name="item" maxOccurs="unbounded" minOccurs="0">
<xs:complexType>
<xs:simpleContent>
<xs:extension base="xs:string">
<xs:attribute type="xs:int" name="id" use="optional"/>
<xs:attribute type="xs:long" name="min" use="optional"/>
<xs:attribute type="xs:long" name="max" use="optional"/>
<xs:attribute type="xs:double" name="chance" use="optional"/>
</xs:extension>
</xs:simpleContent>
</xs:complexType>
</xs:element>
</xs:sequence>
</xs:complexType>
</xs:element>
<xs:element name="lucky" minOccurs="0">
<xs:complexType>
<xs:sequence>
<xs:element name="item" maxOccurs="unbounded" minOccurs="0">
<xs:complexType>
<xs:simpleContent>
<xs:extension base="xs:string">
<xs:attribute type="xs:int" name="id" use="optional"/>
<xs:attribute type="xs:long" name="min" use="optional"/>
<xs:attribute type="xs:long" name="max" use="optional"/>
<xs:attribute type="xs:double" name="chance" use="optional"/>
</xs:extension>
</xs:simpleContent>
</xs:complexType>
</xs:element>
</xs:sequence>
</xs:complexType>
</xs:element>
</xs:sequence>
</xs:complexType>
</xs:element>
<xs:element name="collision" minOccurs="0" maxOccurs="1">

View File

@ -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<Integer> clans = null;
Set<Integer> ignoreClanNpcIds = null;
List<DropHolder> dropLists = null;
List<DropGroupHolder> 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)

View File

@ -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<AISkillScope, List<Skill>> _aiSkillLists;
private Set<Integer> _clans;
private Set<Integer> _ignoreClanNpcIds;
private List<DropGroupHolder> _dropGroups;
private List<DropHolder> _dropListDeath;
private List<DropHolder> _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<DropGroupHolder> 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<DropGroupHolder> getDropGroups()
{
return _dropGroups;
}
public List<DropHolder> getDropList()
{
return _dropListDeath;
@ -685,11 +702,201 @@ public class NpcTemplate extends CreatureTemplate implements IIdentifiable
public List<ItemHolder> calculateDrops(DropType dropType, Creature victim, Creature killer)
{
final List<DropHolder> dropList = dropType == DropType.SPOIL ? _dropListSpoil : _dropListDeath;
if (dropList == null)
if (dropType == DropType.DROP)
{
return null;
// calculate group drops
List<ItemHolder> groupDrops = null;
if (_dropGroups != null)
{
groupDrops = calculateGroupDrops(victim, killer);
}
// calculate ungrouped drops
List<ItemHolder> 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<ItemHolder> 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<ItemHolder> calculatedDrops = null;
for (DropGroupHolder group : _dropGroups)
{
if (dropOccurrenceCounter <= 0)
{
break;
}
double groupRate = 1;
final List<DropHolder> 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<ItemHolder> calculateUngroupedDrops(DropType dropType, Creature victim, Creature killer)
{
final List<DropHolder> 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())
{

View File

@ -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 <http://www.gnu.org/licenses/>.
*/
package org.l2jmobius.gameserver.model.holders;
import java.util.ArrayList;
import java.util.List;
/**
* @author Mobius
*/
public class DropGroupHolder
{
private final List<DropHolder> _dropList = new ArrayList<>();
private final double _chance;
public DropGroupHolder(double chance)
{
_chance = chance;
}
public List<DropHolder> getDropList()
{
return _dropList;
}
public void addDrop(DropHolder holder)
{
_dropList.add(holder);
}
public double getChance()
{
return _chance;
}
}

View File

@ -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<DropGroupHolder> dropListGroups = npc.getTemplate().getDropGroups();
final List<DropHolder> dropListDeath = npc.getTemplate().getDropList();
final List<DropHolder> dropListSpoil = npc.getTemplate().getSpoilList();
if ((dropListDeath != null) || (dropListSpoil != null))
if ((dropListGroups != null) || (dropListDeath != null) || (dropListSpoil != null))
{
sb.append("<table width=275 cellpadding=0 cellspacing=0><tr>");
if (dropListDeath != null)
if ((dropListGroups != null) || (dropListDeath != null))
{
sb.append("<td align=center><button value=\"Show Drop\" width=100 height=25 action=\"bypass NpcViewMod dropList DROP " + npc.getObjectId() + "\" back=\"L2UI_CT1.Button_DF_Calculator_Down\" fore=\"L2UI_CT1.Button_DF_Calculator\"></td>");
}
@ -228,13 +230,40 @@ public class NpcViewMod implements IBypassHandler
private void sendNpcDropList(Player player, Npc npc, DropType dropType, int pageValue)
{
final List<DropHolder> templateList = dropType == DropType.SPOIL ? npc.getTemplate().getSpoilList() : npc.getTemplate().getDropList();
if (templateList == null)
List<DropHolder> dropList = null;
if (dropType == DropType.SPOIL)
{
dropList = new ArrayList<>(npc.getTemplate().getSpoilList());
}
else
{
final List<DropHolder> drops = npc.getTemplate().getDropList();
if (drops != null)
{
dropList = new ArrayList<>(drops);
}
final List<DropGroupHolder> 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<DropHolder> 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;

View File

@ -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())

View File

@ -13,7 +13,67 @@
<xs:element name="ai" type="aiType" minOccurs="0" maxOccurs="1" />
<xs:element name="collision" type="collisionType" minOccurs="0" maxOccurs="1" />
<xs:element name="corpseTime" type="xs:nonNegativeInteger" minOccurs="0" maxOccurs="1" />
<xs:element name="dropLists" type="dropListsType" minOccurs="0" maxOccurs="1" />
<xs:element name="dropLists" minOccurs="0" maxOccurs="1">
<xs:complexType>
<xs:sequence>
<xs:element name="drop" minOccurs="0">
<xs:complexType>
<xs:sequence>
<xs:element name="item" maxOccurs="unbounded" minOccurs="0">
<xs:complexType>
<xs:simpleContent>
<xs:extension base="xs:string">
<xs:attribute type="xs:int" name="id" use="required"/>
<xs:attribute type="xs:long" name="min" use="required"/>
<xs:attribute type="xs:long" name="max" use="required"/>
<xs:attribute type="xs:double" name="chance" use="required"/>
</xs:extension>
</xs:simpleContent>
</xs:complexType>
</xs:element>
<xs:element name="group" maxOccurs="unbounded" minOccurs="0">
<xs:complexType>
<xs:sequence>
<xs:element name="item" maxOccurs="unbounded" minOccurs="0">
<xs:complexType>
<xs:simpleContent>
<xs:extension base="xs:string">
<xs:attribute type="xs:int" name="id" use="required"/>
<xs:attribute type="xs:long" name="min" use="required"/>
<xs:attribute type="xs:long" name="max" use="required"/>
<xs:attribute type="xs:double" name="chance" use="required"/>
</xs:extension>
</xs:simpleContent>
</xs:complexType>
</xs:element>
</xs:sequence>
<xs:attribute type="xs:double" name="chance" use="optional"/>
</xs:complexType>
</xs:element>
</xs:sequence>
</xs:complexType>
</xs:element>
<xs:element name="spoil" minOccurs="0">
<xs:complexType>
<xs:sequence>
<xs:element name="item" maxOccurs="unbounded" minOccurs="0">
<xs:complexType>
<xs:simpleContent>
<xs:extension base="xs:string">
<xs:attribute type="xs:int" name="id" use="optional"/>
<xs:attribute type="xs:long" name="min" use="optional"/>
<xs:attribute type="xs:long" name="max" use="optional"/>
<xs:attribute type="xs:double" name="chance" use="optional"/>
</xs:extension>
</xs:simpleContent>
</xs:complexType>
</xs:element>
</xs:sequence>
</xs:complexType>
</xs:element>
</xs:sequence>
</xs:complexType>
</xs:element>
<xs:element name="equipment" type="equipmentType" minOccurs="0" maxOccurs="1" />
<xs:element name="exCrtEffect" type="xs:boolean" minOccurs="0" maxOccurs="1" />
<xs:element name="parameters" type="parametersType" minOccurs="0" maxOccurs="1" />

View File

@ -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<Integer> clans = null;
Set<Integer> ignoreClanNpcIds = null;
List<DropHolder> dropLists = null;
List<DropGroupHolder> 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)

View File

@ -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<AISkillScope, List<Skill>> _aiSkillLists;
private Set<Integer> _clans;
private Set<Integer> _ignoreClanNpcIds;
private List<DropGroupHolder> _dropGroups;
private List<DropHolder> _dropListDeath;
private List<DropHolder> _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<DropGroupHolder> 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<DropGroupHolder> getDropGroups()
{
return _dropGroups;
}
public List<DropHolder> getDropList()
{
return _dropListDeath;
@ -624,11 +641,201 @@ public class NpcTemplate extends CreatureTemplate implements IIdentifiable
public List<ItemHolder> calculateDrops(DropType dropType, Creature victim, Creature killer)
{
final List<DropHolder> dropList = dropType == DropType.SPOIL ? _dropListSpoil : _dropListDeath;
if (dropList == null)
if (dropType == DropType.DROP)
{
return null;
// calculate group drops
List<ItemHolder> groupDrops = null;
if (_dropGroups != null)
{
groupDrops = calculateGroupDrops(victim, killer);
}
// calculate ungrouped drops
List<ItemHolder> 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<ItemHolder> 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<ItemHolder> calculatedDrops = null;
for (DropGroupHolder group : _dropGroups)
{
if (dropOccurrenceCounter <= 0)
{
break;
}
double groupRate = 1;
final List<DropHolder> 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<ItemHolder> calculateUngroupedDrops(DropType dropType, Creature victim, Creature killer)
{
final List<DropHolder> 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())
{

View File

@ -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 <http://www.gnu.org/licenses/>.
*/
package org.l2jmobius.gameserver.model.holders;
import java.util.ArrayList;
import java.util.List;
/**
* @author Mobius
*/
public class DropGroupHolder
{
private final List<DropHolder> _dropList = new ArrayList<>();
private final double _chance;
public DropGroupHolder(double chance)
{
_chance = chance;
}
public List<DropHolder> getDropList()
{
return _dropList;
}
public void addDrop(DropHolder holder)
{
_dropList.add(holder);
}
public double getChance()
{
return _chance;
}
}

View File

@ -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<DropGroupHolder> dropListGroups = npc.getTemplate().getDropGroups();
final List<DropHolder> dropListDeath = npc.getTemplate().getDropList();
final List<DropHolder> dropListSpoil = npc.getTemplate().getSpoilList();
if ((dropListDeath != null) || (dropListSpoil != null))
if ((dropListGroups != null) || (dropListDeath != null) || (dropListSpoil != null))
{
sb.append("<table width=275 cellpadding=0 cellspacing=0><tr>");
if (dropListDeath != null)
if ((dropListGroups != null) || (dropListDeath != null))
{
sb.append("<td align=center><button value=\"Show Drop\" width=100 height=25 action=\"bypass NpcViewMod dropList DROP " + npc.getObjectId() + "\" back=\"L2UI_CT1.Button_DF_Calculator_Down\" fore=\"L2UI_CT1.Button_DF_Calculator\"></td>");
}
@ -228,13 +230,40 @@ public class NpcViewMod implements IBypassHandler
private void sendNpcDropList(Player player, Npc npc, DropType dropType, int pageValue)
{
final List<DropHolder> templateList = dropType == DropType.SPOIL ? npc.getTemplate().getSpoilList() : npc.getTemplate().getDropList();
if (templateList == null)
List<DropHolder> dropList = null;
if (dropType == DropType.SPOIL)
{
dropList = new ArrayList<>(npc.getTemplate().getSpoilList());
}
else
{
final List<DropHolder> drops = npc.getTemplate().getDropList();
if (drops != null)
{
dropList = new ArrayList<>(drops);
}
final List<DropGroupHolder> 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<DropHolder> 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;

View File

@ -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())

View File

@ -13,7 +13,67 @@
<xs:element name="ai" type="aiType" minOccurs="0" maxOccurs="1" />
<xs:element name="collision" type="collisionType" minOccurs="0" maxOccurs="1" />
<xs:element name="corpseTime" type="xs:nonNegativeInteger" minOccurs="0" maxOccurs="1" />
<xs:element name="dropLists" type="dropListsType" minOccurs="0" maxOccurs="1" />
<xs:element name="dropLists" minOccurs="0" maxOccurs="1">
<xs:complexType>
<xs:sequence>
<xs:element name="drop" minOccurs="0">
<xs:complexType>
<xs:sequence>
<xs:element name="item" maxOccurs="unbounded" minOccurs="0">
<xs:complexType>
<xs:simpleContent>
<xs:extension base="xs:string">
<xs:attribute type="xs:int" name="id" use="required"/>
<xs:attribute type="xs:long" name="min" use="required"/>
<xs:attribute type="xs:long" name="max" use="required"/>
<xs:attribute type="xs:double" name="chance" use="required"/>
</xs:extension>
</xs:simpleContent>
</xs:complexType>
</xs:element>
<xs:element name="group" maxOccurs="unbounded" minOccurs="0">
<xs:complexType>
<xs:sequence>
<xs:element name="item" maxOccurs="unbounded" minOccurs="0">
<xs:complexType>
<xs:simpleContent>
<xs:extension base="xs:string">
<xs:attribute type="xs:int" name="id" use="required"/>
<xs:attribute type="xs:long" name="min" use="required"/>
<xs:attribute type="xs:long" name="max" use="required"/>
<xs:attribute type="xs:double" name="chance" use="required"/>
</xs:extension>
</xs:simpleContent>
</xs:complexType>
</xs:element>
</xs:sequence>
<xs:attribute type="xs:double" name="chance" use="optional"/>
</xs:complexType>
</xs:element>
</xs:sequence>
</xs:complexType>
</xs:element>
<xs:element name="spoil" minOccurs="0">
<xs:complexType>
<xs:sequence>
<xs:element name="item" maxOccurs="unbounded" minOccurs="0">
<xs:complexType>
<xs:simpleContent>
<xs:extension base="xs:string">
<xs:attribute type="xs:int" name="id" use="optional"/>
<xs:attribute type="xs:long" name="min" use="optional"/>
<xs:attribute type="xs:long" name="max" use="optional"/>
<xs:attribute type="xs:double" name="chance" use="optional"/>
</xs:extension>
</xs:simpleContent>
</xs:complexType>
</xs:element>
</xs:sequence>
</xs:complexType>
</xs:element>
</xs:sequence>
</xs:complexType>
</xs:element>
<xs:element name="equipment" type="equipmentType" minOccurs="0" maxOccurs="1" />
<xs:element name="exCrtEffect" type="xs:boolean" minOccurs="0" maxOccurs="1" />
<xs:element name="parameters" type="parametersType" minOccurs="0" maxOccurs="1" />

View File

@ -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<Integer> clans = null;
Set<Integer> ignoreClanNpcIds = null;
List<DropHolder> dropLists = null;
List<DropGroupHolder> 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)

View File

@ -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<AISkillScope, List<Skill>> _aiSkillLists;
private Set<Integer> _clans;
private Set<Integer> _ignoreClanNpcIds;
private List<DropGroupHolder> _dropGroups;
private List<DropHolder> _dropListDeath;
private List<DropHolder> _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<DropGroupHolder> 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<DropGroupHolder> getDropGroups()
{
return _dropGroups;
}
public List<DropHolder> getDropList()
{
return _dropListDeath;
@ -624,11 +641,201 @@ public class NpcTemplate extends CreatureTemplate implements IIdentifiable
public List<ItemHolder> calculateDrops(DropType dropType, Creature victim, Creature killer)
{
final List<DropHolder> dropList = dropType == DropType.SPOIL ? _dropListSpoil : _dropListDeath;
if (dropList == null)
if (dropType == DropType.DROP)
{
return null;
// calculate group drops
List<ItemHolder> groupDrops = null;
if (_dropGroups != null)
{
groupDrops = calculateGroupDrops(victim, killer);
}
// calculate ungrouped drops
List<ItemHolder> 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<ItemHolder> 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<ItemHolder> calculatedDrops = null;
for (DropGroupHolder group : _dropGroups)
{
if (dropOccurrenceCounter <= 0)
{
break;
}
double groupRate = 1;
final List<DropHolder> 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<ItemHolder> calculateUngroupedDrops(DropType dropType, Creature victim, Creature killer)
{
final List<DropHolder> 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())
{

View File

@ -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 <http://www.gnu.org/licenses/>.
*/
package org.l2jmobius.gameserver.model.holders;
import java.util.ArrayList;
import java.util.List;
/**
* @author Mobius
*/
public class DropGroupHolder
{
private final List<DropHolder> _dropList = new ArrayList<>();
private final double _chance;
public DropGroupHolder(double chance)
{
_chance = chance;
}
public List<DropHolder> getDropList()
{
return _dropList;
}
public void addDrop(DropHolder holder)
{
_dropList.add(holder);
}
public double getChance()
{
return _chance;
}
}

View File

@ -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<DropGroupHolder> dropListGroups = npc.getTemplate().getDropGroups();
final List<DropHolder> dropListDeath = npc.getTemplate().getDropList();
final List<DropHolder> dropListSpoil = npc.getTemplate().getSpoilList();
if ((dropListDeath != null) || (dropListSpoil != null))
if ((dropListGroups != null) || (dropListDeath != null) || (dropListSpoil != null))
{
sb.append("<table width=275 cellpadding=0 cellspacing=0><tr>");
if (dropListDeath != null)
if ((dropListGroups != null) || (dropListDeath != null))
{
sb.append("<td align=center><button value=\"Show Drop\" width=100 height=25 action=\"bypass NpcViewMod dropList DROP " + npc.getObjectId() + "\" back=\"L2UI_CT1.Button_DF_Calculator_Down\" fore=\"L2UI_CT1.Button_DF_Calculator\"></td>");
}
@ -348,13 +350,40 @@ public class NpcViewMod implements IBypassHandler
private void sendNpcDropList(Player player, Npc npc, DropType dropType, int pageValue)
{
final List<DropHolder> templateList = dropType == DropType.SPOIL ? npc.getTemplate().getSpoilList() : npc.getTemplate().getDropList();
if (templateList == null)
List<DropHolder> dropList = null;
if (dropType == DropType.SPOIL)
{
dropList = new ArrayList<>(npc.getTemplate().getSpoilList());
}
else
{
final List<DropHolder> drops = npc.getTemplate().getDropList();
if (drops != null)
{
dropList = new ArrayList<>(drops);
}
final List<DropGroupHolder> 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<DropHolder> 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;

View File

@ -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())

View File

@ -1,16 +1,5 @@
<?xml version="1.0" encoding="UTF-8"?>
<xs:schema xmlns:xs="http://www.w3.org/2001/XMLSchema">
<xs:complexType name="dropListItem">
<xs:attribute name="id" type="xs:positiveInteger" use="required" />
<xs:attribute name="min" type="xs:nonNegativeInteger" />
<xs:attribute name="max" type="xs:positiveInteger" />
<xs:attribute name="chance" type="xs:decimal" />
</xs:complexType>
<xs:complexType name="dropList">
<xs:choice minOccurs="1" maxOccurs="unbounded">
<xs:element name="item" type="dropListItem" />
</xs:choice>
</xs:complexType>
<xs:element name="list">
<xs:complexType>
<xs:sequence>
@ -260,11 +249,81 @@
</xs:element>
<xs:element name="dropLists" minOccurs="0" maxOccurs="1">
<xs:complexType>
<xs:all>
<xs:element name="drop" type="dropList" minOccurs="0" maxOccurs="1" />
<xs:element name="spoil" type="dropList" minOccurs="0" maxOccurs="1" />
<xs:element name="lucky" type="dropList" minOccurs="0" maxOccurs="1" />
</xs:all>
<xs:sequence>
<xs:element name="drop" minOccurs="0">
<xs:complexType>
<xs:sequence>
<xs:element name="item" maxOccurs="unbounded" minOccurs="0">
<xs:complexType>
<xs:simpleContent>
<xs:extension base="xs:string">
<xs:attribute type="xs:int" name="id" use="required"/>
<xs:attribute type="xs:long" name="min" use="required"/>
<xs:attribute type="xs:long" name="max" use="required"/>
<xs:attribute type="xs:double" name="chance" use="required"/>
</xs:extension>
</xs:simpleContent>
</xs:complexType>
</xs:element>
<xs:element name="group" maxOccurs="unbounded" minOccurs="0">
<xs:complexType>
<xs:sequence>
<xs:element name="item" maxOccurs="unbounded" minOccurs="0">
<xs:complexType>
<xs:simpleContent>
<xs:extension base="xs:string">
<xs:attribute type="xs:int" name="id" use="required"/>
<xs:attribute type="xs:long" name="min" use="required"/>
<xs:attribute type="xs:long" name="max" use="required"/>
<xs:attribute type="xs:double" name="chance" use="required"/>
</xs:extension>
</xs:simpleContent>
</xs:complexType>
</xs:element>
</xs:sequence>
<xs:attribute type="xs:double" name="chance" use="optional"/>
</xs:complexType>
</xs:element>
</xs:sequence>
</xs:complexType>
</xs:element>
<xs:element name="spoil" minOccurs="0">
<xs:complexType>
<xs:sequence>
<xs:element name="item" maxOccurs="unbounded" minOccurs="0">
<xs:complexType>
<xs:simpleContent>
<xs:extension base="xs:string">
<xs:attribute type="xs:int" name="id" use="optional"/>
<xs:attribute type="xs:long" name="min" use="optional"/>
<xs:attribute type="xs:long" name="max" use="optional"/>
<xs:attribute type="xs:double" name="chance" use="optional"/>
</xs:extension>
</xs:simpleContent>
</xs:complexType>
</xs:element>
</xs:sequence>
</xs:complexType>
</xs:element>
<xs:element name="lucky" minOccurs="0">
<xs:complexType>
<xs:sequence>
<xs:element name="item" maxOccurs="unbounded" minOccurs="0">
<xs:complexType>
<xs:simpleContent>
<xs:extension base="xs:string">
<xs:attribute type="xs:int" name="id" use="optional"/>
<xs:attribute type="xs:long" name="min" use="optional"/>
<xs:attribute type="xs:long" name="max" use="optional"/>
<xs:attribute type="xs:double" name="chance" use="optional"/>
</xs:extension>
</xs:simpleContent>
</xs:complexType>
</xs:element>
</xs:sequence>
</xs:complexType>
</xs:element>
</xs:sequence>
</xs:complexType>
</xs:element>
<xs:element name="collision" minOccurs="0" maxOccurs="1">

View File

@ -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<Integer> clans = null;
Set<Integer> ignoreClanNpcIds = null;
List<DropHolder> dropLists = null;
List<DropGroupHolder> 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)

View File

@ -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<AISkillScope, List<Skill>> _aiSkillLists;
private Set<Integer> _clans;
private Set<Integer> _ignoreClanNpcIds;
private List<DropGroupHolder> _dropGroups;
private List<DropHolder> _dropListDeath;
private List<DropHolder> _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<DropGroupHolder> 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<DropGroupHolder> getDropGroups()
{
return _dropGroups;
}
public List<DropHolder> getDropList()
{
return _dropListDeath;
@ -686,11 +703,201 @@ public class NpcTemplate extends CreatureTemplate implements IIdentifiable
public List<ItemHolder> calculateDrops(DropType dropType, Creature victim, Creature killer)
{
final List<DropHolder> dropList = dropType == DropType.SPOIL ? _dropListSpoil : _dropListDeath;
if (dropList == null)
if (dropType == DropType.DROP)
{
return null;
// calculate group drops
List<ItemHolder> groupDrops = null;
if (_dropGroups != null)
{
groupDrops = calculateGroupDrops(victim, killer);
}
// calculate ungrouped drops
List<ItemHolder> 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<ItemHolder> 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<ItemHolder> calculatedDrops = null;
for (DropGroupHolder group : _dropGroups)
{
if (dropOccurrenceCounter <= 0)
{
break;
}
double groupRate = 1;
final List<DropHolder> 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<ItemHolder> calculateUngroupedDrops(DropType dropType, Creature victim, Creature killer)
{
final List<DropHolder> 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())
{

View File

@ -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 <http://www.gnu.org/licenses/>.
*/
package org.l2jmobius.gameserver.model.holders;
import java.util.ArrayList;
import java.util.List;
/**
* @author Mobius
*/
public class DropGroupHolder
{
private final List<DropHolder> _dropList = new ArrayList<>();
private final double _chance;
public DropGroupHolder(double chance)
{
_chance = chance;
}
public List<DropHolder> getDropList()
{
return _dropList;
}
public void addDrop(DropHolder holder)
{
_dropList.add(holder);
}
public double getChance()
{
return _chance;
}
}

View File

@ -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<DropGroupHolder> dropListGroups = npc.getTemplate().getDropGroups();
final List<DropHolder> dropListDeath = npc.getTemplate().getDropList();
final List<DropHolder> dropListSpoil = npc.getTemplate().getSpoilList();
if ((dropListDeath != null) || (dropListSpoil != null))
if ((dropListGroups != null) || (dropListDeath != null) || (dropListSpoil != null))
{
sb.append("<table width=275 cellpadding=0 cellspacing=0><tr>");
if (dropListDeath != null)
if ((dropListGroups != null) || (dropListDeath != null))
{
sb.append("<td align=center><button value=\"Show Drop\" width=100 height=25 action=\"bypass NpcViewMod dropList DROP " + npc.getObjectId() + "\" back=\"L2UI_CT1.Button_DF_Calculator_Down\" fore=\"L2UI_CT1.Button_DF_Calculator\"></td>");
}
@ -348,13 +350,40 @@ public class NpcViewMod implements IBypassHandler
private void sendNpcDropList(Player player, Npc npc, DropType dropType, int pageValue)
{
final List<DropHolder> templateList = dropType == DropType.SPOIL ? npc.getTemplate().getSpoilList() : npc.getTemplate().getDropList();
if (templateList == null)
List<DropHolder> dropList = null;
if (dropType == DropType.SPOIL)
{
dropList = new ArrayList<>(npc.getTemplate().getSpoilList());
}
else
{
final List<DropHolder> drops = npc.getTemplate().getDropList();
if (drops != null)
{
dropList = new ArrayList<>(drops);
}
final List<DropGroupHolder> 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<DropHolder> 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;

View File

@ -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())

View File

@ -1,16 +1,5 @@
<?xml version="1.0" encoding="UTF-8"?>
<xs:schema xmlns:xs="http://www.w3.org/2001/XMLSchema">
<xs:complexType name="dropListItem">
<xs:attribute name="id" type="xs:positiveInteger" use="required" />
<xs:attribute name="min" type="xs:nonNegativeInteger" />
<xs:attribute name="max" type="xs:positiveInteger" />
<xs:attribute name="chance" type="xs:decimal" />
</xs:complexType>
<xs:complexType name="dropList">
<xs:choice minOccurs="1" maxOccurs="unbounded">
<xs:element name="item" type="dropListItem" />
</xs:choice>
</xs:complexType>
<xs:element name="list">
<xs:complexType>
<xs:sequence>
@ -260,11 +249,81 @@
</xs:element>
<xs:element name="dropLists" minOccurs="0" maxOccurs="1">
<xs:complexType>
<xs:all>
<xs:element name="drop" type="dropList" minOccurs="0" maxOccurs="1" />
<xs:element name="spoil" type="dropList" minOccurs="0" maxOccurs="1" />
<xs:element name="lucky" type="dropList" minOccurs="0" maxOccurs="1" />
</xs:all>
<xs:sequence>
<xs:element name="drop" minOccurs="0">
<xs:complexType>
<xs:sequence>
<xs:element name="item" maxOccurs="unbounded" minOccurs="0">
<xs:complexType>
<xs:simpleContent>
<xs:extension base="xs:string">
<xs:attribute type="xs:int" name="id" use="required"/>
<xs:attribute type="xs:long" name="min" use="required"/>
<xs:attribute type="xs:long" name="max" use="required"/>
<xs:attribute type="xs:double" name="chance" use="required"/>
</xs:extension>
</xs:simpleContent>
</xs:complexType>
</xs:element>
<xs:element name="group" maxOccurs="unbounded" minOccurs="0">
<xs:complexType>
<xs:sequence>
<xs:element name="item" maxOccurs="unbounded" minOccurs="0">
<xs:complexType>
<xs:simpleContent>
<xs:extension base="xs:string">
<xs:attribute type="xs:int" name="id" use="required"/>
<xs:attribute type="xs:long" name="min" use="required"/>
<xs:attribute type="xs:long" name="max" use="required"/>
<xs:attribute type="xs:double" name="chance" use="required"/>
</xs:extension>
</xs:simpleContent>
</xs:complexType>
</xs:element>
</xs:sequence>
<xs:attribute type="xs:double" name="chance" use="optional"/>
</xs:complexType>
</xs:element>
</xs:sequence>
</xs:complexType>
</xs:element>
<xs:element name="spoil" minOccurs="0">
<xs:complexType>
<xs:sequence>
<xs:element name="item" maxOccurs="unbounded" minOccurs="0">
<xs:complexType>
<xs:simpleContent>
<xs:extension base="xs:string">
<xs:attribute type="xs:int" name="id" use="optional"/>
<xs:attribute type="xs:long" name="min" use="optional"/>
<xs:attribute type="xs:long" name="max" use="optional"/>
<xs:attribute type="xs:double" name="chance" use="optional"/>
</xs:extension>
</xs:simpleContent>
</xs:complexType>
</xs:element>
</xs:sequence>
</xs:complexType>
</xs:element>
<xs:element name="lucky" minOccurs="0">
<xs:complexType>
<xs:sequence>
<xs:element name="item" maxOccurs="unbounded" minOccurs="0">
<xs:complexType>
<xs:simpleContent>
<xs:extension base="xs:string">
<xs:attribute type="xs:int" name="id" use="optional"/>
<xs:attribute type="xs:long" name="min" use="optional"/>
<xs:attribute type="xs:long" name="max" use="optional"/>
<xs:attribute type="xs:double" name="chance" use="optional"/>
</xs:extension>
</xs:simpleContent>
</xs:complexType>
</xs:element>
</xs:sequence>
</xs:complexType>
</xs:element>
</xs:sequence>
</xs:complexType>
</xs:element>
<xs:element name="collision" minOccurs="0" maxOccurs="1">

View File

@ -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<Integer> clans = null;
Set<Integer> ignoreClanNpcIds = null;
List<DropHolder> dropLists = null;
List<DropGroupHolder> 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)

View File

@ -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<AISkillScope, List<Skill>> _aiSkillLists;
private Set<Integer> _clans;
private Set<Integer> _ignoreClanNpcIds;
private List<DropGroupHolder> _dropGroups;
private List<DropHolder> _dropListDeath;
private List<DropHolder> _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<DropGroupHolder> 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<DropGroupHolder> getDropGroups()
{
return _dropGroups;
}
public List<DropHolder> getDropList()
{
return _dropListDeath;
@ -686,11 +703,201 @@ public class NpcTemplate extends CreatureTemplate implements IIdentifiable
public List<ItemHolder> calculateDrops(DropType dropType, Creature victim, Creature killer)
{
final List<DropHolder> dropList = dropType == DropType.SPOIL ? _dropListSpoil : _dropListDeath;
if (dropList == null)
if (dropType == DropType.DROP)
{
return null;
// calculate group drops
List<ItemHolder> groupDrops = null;
if (_dropGroups != null)
{
groupDrops = calculateGroupDrops(victim, killer);
}
// calculate ungrouped drops
List<ItemHolder> 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<ItemHolder> 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<ItemHolder> calculatedDrops = null;
for (DropGroupHolder group : _dropGroups)
{
if (dropOccurrenceCounter <= 0)
{
break;
}
double groupRate = 1;
final List<DropHolder> 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<ItemHolder> calculateUngroupedDrops(DropType dropType, Creature victim, Creature killer)
{
final List<DropHolder> 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())
{

View File

@ -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 <http://www.gnu.org/licenses/>.
*/
package org.l2jmobius.gameserver.model.holders;
import java.util.ArrayList;
import java.util.List;
/**
* @author Mobius
*/
public class DropGroupHolder
{
private final List<DropHolder> _dropList = new ArrayList<>();
private final double _chance;
public DropGroupHolder(double chance)
{
_chance = chance;
}
public List<DropHolder> getDropList()
{
return _dropList;
}
public void addDrop(DropHolder holder)
{
_dropList.add(holder);
}
public double getChance()
{
return _chance;
}
}

View File

@ -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<DropGroupHolder> dropListGroups = npc.getTemplate().getDropGroups();
final List<DropHolder> dropListDeath = npc.getTemplate().getDropList();
final List<DropHolder> dropListSpoil = npc.getTemplate().getSpoilList();
if ((dropListDeath != null) || (dropListSpoil != null))
if ((dropListGroups != null) || (dropListDeath != null) || (dropListSpoil != null))
{
sb.append("<table width=275 cellpadding=0 cellspacing=0><tr>");
if (dropListDeath != null)
if ((dropListGroups != null) || (dropListDeath != null))
{
sb.append("<td align=center><button value=\"Show Drop\" width=100 height=25 action=\"bypass NpcViewMod dropList DROP " + npc.getObjectId() + "\" back=\"L2UI_CT1.Button_DF_Calculator_Down\" fore=\"L2UI_CT1.Button_DF_Calculator\"></td>");
}
@ -348,13 +350,40 @@ public class NpcViewMod implements IBypassHandler
private void sendNpcDropList(Player player, Npc npc, DropType dropType, int pageValue)
{
final List<DropHolder> templateList = dropType == DropType.SPOIL ? npc.getTemplate().getSpoilList() : npc.getTemplate().getDropList();
if (templateList == null)
List<DropHolder> dropList = null;
if (dropType == DropType.SPOIL)
{
dropList = new ArrayList<>(npc.getTemplate().getSpoilList());
}
else
{
final List<DropHolder> drops = npc.getTemplate().getDropList();
if (drops != null)
{
dropList = new ArrayList<>(drops);
}
final List<DropGroupHolder> 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<DropHolder> 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;

View File

@ -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())

View File

@ -1,16 +1,5 @@
<?xml version="1.0" encoding="UTF-8"?>
<xs:schema xmlns:xs="http://www.w3.org/2001/XMLSchema">
<xs:complexType name="dropListItem">
<xs:attribute name="id" type="xs:positiveInteger" use="required" />
<xs:attribute name="min" type="xs:nonNegativeInteger" />
<xs:attribute name="max" type="xs:positiveInteger" />
<xs:attribute name="chance" type="xs:decimal" />
</xs:complexType>
<xs:complexType name="dropList">
<xs:choice minOccurs="1" maxOccurs="unbounded">
<xs:element name="item" type="dropListItem" />
</xs:choice>
</xs:complexType>
<xs:element name="list">
<xs:complexType>
<xs:sequence>
@ -260,11 +249,81 @@
</xs:element>
<xs:element name="dropLists" minOccurs="0" maxOccurs="1">
<xs:complexType>
<xs:all>
<xs:element name="drop" type="dropList" minOccurs="0" maxOccurs="1" />
<xs:element name="spoil" type="dropList" minOccurs="0" maxOccurs="1" />
<xs:element name="lucky" type="dropList" minOccurs="0" maxOccurs="1" />
</xs:all>
<xs:sequence>
<xs:element name="drop" minOccurs="0">
<xs:complexType>
<xs:sequence>
<xs:element name="item" maxOccurs="unbounded" minOccurs="0">
<xs:complexType>
<xs:simpleContent>
<xs:extension base="xs:string">
<xs:attribute type="xs:int" name="id" use="required"/>
<xs:attribute type="xs:long" name="min" use="required"/>
<xs:attribute type="xs:long" name="max" use="required"/>
<xs:attribute type="xs:double" name="chance" use="required"/>
</xs:extension>
</xs:simpleContent>
</xs:complexType>
</xs:element>
<xs:element name="group" maxOccurs="unbounded" minOccurs="0">
<xs:complexType>
<xs:sequence>
<xs:element name="item" maxOccurs="unbounded" minOccurs="0">
<xs:complexType>
<xs:simpleContent>
<xs:extension base="xs:string">
<xs:attribute type="xs:int" name="id" use="required"/>
<xs:attribute type="xs:long" name="min" use="required"/>
<xs:attribute type="xs:long" name="max" use="required"/>
<xs:attribute type="xs:double" name="chance" use="required"/>
</xs:extension>
</xs:simpleContent>
</xs:complexType>
</xs:element>
</xs:sequence>
<xs:attribute type="xs:double" name="chance" use="optional"/>
</xs:complexType>
</xs:element>
</xs:sequence>
</xs:complexType>
</xs:element>
<xs:element name="spoil" minOccurs="0">
<xs:complexType>
<xs:sequence>
<xs:element name="item" maxOccurs="unbounded" minOccurs="0">
<xs:complexType>
<xs:simpleContent>
<xs:extension base="xs:string">
<xs:attribute type="xs:int" name="id" use="optional"/>
<xs:attribute type="xs:long" name="min" use="optional"/>
<xs:attribute type="xs:long" name="max" use="optional"/>
<xs:attribute type="xs:double" name="chance" use="optional"/>
</xs:extension>
</xs:simpleContent>
</xs:complexType>
</xs:element>
</xs:sequence>
</xs:complexType>
</xs:element>
<xs:element name="lucky" minOccurs="0">
<xs:complexType>
<xs:sequence>
<xs:element name="item" maxOccurs="unbounded" minOccurs="0">
<xs:complexType>
<xs:simpleContent>
<xs:extension base="xs:string">
<xs:attribute type="xs:int" name="id" use="optional"/>
<xs:attribute type="xs:long" name="min" use="optional"/>
<xs:attribute type="xs:long" name="max" use="optional"/>
<xs:attribute type="xs:double" name="chance" use="optional"/>
</xs:extension>
</xs:simpleContent>
</xs:complexType>
</xs:element>
</xs:sequence>
</xs:complexType>
</xs:element>
</xs:sequence>
</xs:complexType>
</xs:element>
<xs:element name="collision" minOccurs="0" maxOccurs="1">

View File

@ -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<Integer> clans = null;
Set<Integer> ignoreClanNpcIds = null;
List<DropHolder> dropLists = null;
List<DropGroupHolder> 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)

Some files were not shown because too many files have changed in this diff Show More