/* * 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 2, 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, write to the Free Software * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA * 02111-1307, USA. * * http://www.gnu.org/copyleft/gpl.html */ package net.sf.l2j.gameserver.model; import java.util.Collections; import java.util.List; import java.util.Map; import java.util.Random; import java.util.WeakHashMap; import java.util.logging.Level; import java.util.logging.Logger; import javolution.util.FastList; import javolution.util.FastMap; import net.sf.l2j.Config; import net.sf.l2j.gameserver.EventDroplist; import net.sf.l2j.gameserver.ItemTable; import net.sf.l2j.gameserver.ItemsAutoDestroy; import net.sf.l2j.gameserver.EventDroplist.DateDrop; import net.sf.l2j.gameserver.ai.CtrlEvent; import net.sf.l2j.gameserver.ai.CtrlIntention; import net.sf.l2j.gameserver.ai.L2AttackableAI; import net.sf.l2j.gameserver.ai.L2CharacterAI; import net.sf.l2j.gameserver.lib.Rnd; import net.sf.l2j.gameserver.model.actor.attackable.AttackableKnownList; import net.sf.l2j.gameserver.model.quest.QuestState; import net.sf.l2j.gameserver.serverpackets.ItemList; import net.sf.l2j.gameserver.serverpackets.PlaySound; import net.sf.l2j.gameserver.serverpackets.SystemMessage; import net.sf.l2j.gameserver.skills.Stats; import net.sf.l2j.gameserver.templates.L2NpcTemplate; /** * This class manages all NPC that can be attacked.

* * L2Attackable :

*
  • L2ArtefactInstance
  • *
  • L2FriendlyMobInstance
  • *
  • L2MonsterInstance
  • *
  • L2SiegeGuardInstance
  • * * @version $Revision: 1.24.2.3.2.16 $ $Date: 2005/04/11 19:11:21 $ */ public class L2Attackable extends L2NpcInstance { static Logger _log = Logger.getLogger(L2Attackable.class.getName()); /** * This class contains all AggroInfo of the L2Attackable against the attacker L2Character.

    * * Data :

    *
  • attacker : The attaker L2Character concerned by this AggroInfo of this L2Attackable
  • *
  • hate : Hate level of this L2Attackable against the attaker L2Character (hate = damage)
  • *
  • damage : Number of damages that the attaker L2Character gave to this L2Attackable


  • * */ public final class AggroInfo { /** The attaker L2Character concerned by this AggroInfo of this L2Attackable */ L2Character attacker; /** Hate level of this L2Attackable against the attaker L2Character (hate = damage) */ int hate; /** Number of damages that the attaker L2Character gave to this L2Attackable */ int damage; /** * Constructor of AggroInfo.

    */ AggroInfo(L2Character attacker) { this.attacker = attacker; } /** * Verify is object is equal to this AggroInfo.

    */ public boolean equals(Object obj) { return this == obj || this.attacker == attacker; } /** * Return the Identifier of the attaker L2Character.

    */ public int hashCode() { return this.attacker.getObjectId(); } } /** * This class contains all RewardInfo of the L2Attackable against the any attacker L2Character, based on amount of damage done.

    * * Data :

    *
  • attacker : The attaker L2Character concerned by this RewardInfo of this L2Attackable
  • *
  • dmg : Total amount of damage done by the attacker to this L2Attackable (summon + own)
  • * */ protected final class RewardInfo { protected L2Character attacker; protected int dmg = 0; public RewardInfo(L2Character attacker, int dmg) { this.attacker = attacker; this.dmg = dmg; } public void addDamage(int dmg) { this.dmg += dmg; } public boolean equals(Object obj) { return this == obj || this.attacker == attacker; } public int hashCode() { return this.attacker.getObjectId(); } } /** * This class contains all AbsorberInfo of the L2Attackable against the absorber L2Character.

    * * Data :

    *
  • absorber : The attaker L2Character concerned by this AbsorberInfo of this L2Attackable
  • * */ public final class AbsorberInfo { /** The attaker L2Character concerned by this AbsorberInfo of this L2Attackable */ L2PcInstance absorber; int crystalId; double absorbedHP; /** * Constructor of AbsorberInfo.

    */ AbsorberInfo(L2PcInstance attacker, int crystalId, double absorbedHP) { this.absorber = attacker; this.crystalId = crystalId; this.absorbedHP = absorbedHP; } /** * Verify is object is equal to this AbsorberInfo.

    */ public boolean equals(Object obj) { return this == obj || this.absorber == absorber; } /** * Return the Identifier of the absorber L2Character.

    */ public int hashCode() { return this.absorber.getObjectId(); } } /** The table containing all autoAttackable L2Character in its Aggro Range and L2Character that attacked the L2Attackable */ private Map _aggroList; public final Map getAggroList() { return _aggroList; } public final void setAggroList(Map aggroList) { _aggroList = aggroList; } /** Table containing all Items that a Dwarf can Sweep on this L2Attackable */ private L2ItemInstance[] _sweepItems; /** crops */ private L2ItemInstance[] _harvestItems; private boolean _seeded; private int _seedType; private Random _rand; /** True if a Soul Crystal was successfuly used on the L2Attackable */ private boolean _absorbed; /** The table containing all L2PcInstance that successfuly absorbed the soul of this L2Attackable */ private Map _absorbersList; /** Static tables containing all mobIDs that can have their souls absorbed, divided by max soul crystal level */ private int[] _absorbingMOBS_level4 = { 583, 584, 585, 586, 587, 588, 636, 637, 638, 639, 640, 641, 642, 643, 644, 645}; private int[] _absorbingMOBS_level8 = { 646, 647, 648, 649, 650, 1006, 1007, 1008}; private int[] _absorbingMOBS_level10 = { 627, 628, 629, 674, 761, 762, 821, 823, 826, 827, 828, 829, 830, 831, 858, 859, 860, 1009, 1010, 1062, 1063, 1068, 1070}; private int[] _absorbingMOBS_level12 = { 10283, 10286, 12221, 12372, 12374}; /** Soul Crystal Basic Informations */ // First ID of each soul crystal private static final int _REDCRYSTAL_OFFSET = 4629; private static final int _REDCRYSTAL_LVL11 = 5577; private static final int _REDCRYSTAL_LVL12 = 5580; private static final int _GREENCRYSTAL_OFFSET = 4640; private static final int _GREENCRYSTAL_LVL11 = 5578; private static final int _GREENCRYSTAL_LVL12 = 5581; private static final int _BLUECRYSTAL_OFFSET = 4651; private static final int _BLUECRYSTAL_LVL11 = 5579; private static final int _BLUECRYSTAL_LVL12 = 5582; // Max number of levels a soul crystal may reach private static final int _MAX_CRYSTALS_LEVEL = 12; // Chances for each event of a soul crystal private static final double _BREAK_CHANCE = 10; private static final double _LEVEL_CHANCE = 32.5; /** End of Soul Crystal Basic Informations */ /** * Constructor of L2Attackable (use L2Character and L2NpcInstance constructor).

    * * Actions :

    *
  • Call the L2Character constructor to set the _template of the L2Attackable (copy skills from template to object and link _calculators to NPC_STD_CALCULATOR)
  • *
  • Set the name of the L2Attackable
  • *
  • Create a RandomAnimation Task that will be launched after the calculated delay if the server allow it


  • * * @param objectId Identifier of the object to initialized * @param L2NpcTemplate Template to apply to the NPC */ public L2Attackable(int objectId, L2NpcTemplate template) { super(objectId, template); super.setKnownList(new AttackableKnownList(new L2Attackable[] {this})); } public AttackableKnownList getKnownList() { return (AttackableKnownList)super.getKnownList(); } /** * Return the L2Character AI of the L2Attackable and if its null create a new one.

    */ public L2CharacterAI getAI() { if (_ai == null) { synchronized(this) { if (_ai == null) _ai = new L2AttackableAI(new AIAccessor()); } } return _ai; } /** * Return the distance under which the object must be add to _knownObject in function of the object type.

    * * Values :

    *
  • object is a L2FolkInstance : 0 (don't remember it)
  • *
  • object is a L2Character : 0 (don't remember it)
  • *
  • object is a L2PlayableInstance : 1500
  • *
  • Aggro Range > Faction Range : Aggro Range
  • *
  • Faction Range > 200 : Faction Range
  • *
  • others : 200


  • * * @param object The Object to add to _knownObject * */ public int getDistanceToWatchObject(L2Object object) { if (object instanceof L2FolkInstance || !(object instanceof L2Character)) return 0; if (object instanceof L2PlayableInstance) return 1500; if (getAggroRange() > getFactionRange()) return getAggroRange(); if (getFactionRange() > 200) return getFactionRange(); return 200; } /** * Return the distance after which the object must be remove from _knownObject in function of the object type.

    * * Values :

    *
  • aggroList is not null : 3000
  • *
  • object is a L2FolkInstance : 0 (don't remember it)
  • *
  • object is a L2Character : 0 (don't remember it)
  • *
  • object is a L2PlayableInstance : 3000
  • *
  • Aggro Range > Faction Range : 2* Aggro Range
  • *
  • Faction Range > 200 : 2* Faction Range
  • *
  • others : 400


  • * * @param object The Object to remove from _knownObject * */ public int getDistanceToForgetObject(L2Object object) { if (_aggroList != null) { synchronized (_aggroList) { if (_aggroList.get(object) != null) return 3000; } } return 2*getDistanceToWatchObject(object); } // get condition to hate, actually isAggressive() is checked // by monster and karma by guards in motheds that overwrite this one. /** * Not used.

    * * @deprecated * */ public boolean getCondition2(L2Character target) { if (target instanceof L2FolkInstance || target instanceof L2DoorInstance) return false; if (target.isAlikeDead() || getDistance(target.getX(), target.getY()) > getAggroRange() || Math.abs(getZ()-target.getZ()) > 100 ) return false; if (target instanceof L2PcInstance) return !((L2PcInstance)target).isInvul(); return true; } /** * Reduce the current HP of the L2Attackable.

    * * @param damage The HP decrease value * @param attacker The L2Character who attacks * */ public void reduceCurrentHp(double damage, L2Character attacker) { reduceCurrentHp(damage, attacker, true); } /** * Reduce the current HP of the L2Attackable, update its _aggroList and launch the doDie Task if necessary.

    * * @param i The HP decrease value * @param attacker The L2Character who attacks * @param awake The awake state (If True : stop sleeping) * */ public void reduceCurrentHp(double damage, L2Character attacker, boolean awake) { if ((this instanceof L2SiegeGuardInstance) && (attacker instanceof L2SiegeGuardInstance)) //if((this.getEffect(L2Effect.EffectType.CONFUSION)!=null) && (attacker.getEffect(L2Effect.EffectType.CONFUSION)!=null)) return; if ((this instanceof L2MonsterInstance)&&(attacker instanceof L2MonsterInstance)) //if((this.getEffect(L2Effect.EffectType.CONFUSION)!=null) && (attacker.getEffect(L2Effect.EffectType.CONFUSION)!=null)) return; // Add damage and hate to the attacker AggroInfo of the L2Attackable _aggroList if (attacker != null) addDamage(attacker, (int)damage); // If this L2Attackable is a L2MonsterInstance and it has spawned minions, call its minions to battle if (this instanceof L2MonsterInstance) { L2MonsterInstance master = (L2MonsterInstance) this; if (this instanceof L2MinionInstance) { master = ((L2MinionInstance)this).getLeader(); if (!master.isInCombat()) master.addDamage(attacker, 1); } if (master.hasMinions()) master.callMinionsToAssist(attacker); } // Reduce the current HP of the L2Attackable and launch the doDie Task if necessary super.reduceCurrentHp(damage, attacker, awake); } /** * Kill the L2Attackable (the corpse disappeared after 7 seconds), distribute rewards (EXP, SP, Drops...) and notify Quest Engine.

    * * Actions :

    *
  • Distribute Exp and SP rewards to L2PcInstance (including Summon owner) that hit the L2Attackable and to their Party members
  • *
  • Notify the Quest Engine of the L2Attackable death if necessary
  • *
  • Kill the L2NpcInstance (the corpse disappeared after 7 seconds)


  • * * Caution : This method DOESN'T GIVE rewards to L2PetInstance

    * * @param killer The L2Character that has killed the L2Attackable * */ public void doDie(L2Character killer) { // Enhance soul crystals of the attacker if this L2Attackable had its soul absorbed try { if (killer instanceof L2PcInstance) { levelSoulCrystals(killer); } } catch (Exception e) { _log.log(Level.SEVERE, "", e); } // Distribute Exp and SP rewards to L2PcInstance (including Summon owner) that hit the L2Attackable and to their Party members try { calculateRewards(killer); } catch (Exception e) { _log.log(Level.SEVERE, "", e); } // Notify the Quest Engine of the L2Attackable death if necessary try { if (killer instanceof L2PcInstance || killer instanceof L2SummonInstance) { // Get the L2PcInstance that killed the L2Attackable L2PcInstance player = killer instanceof L2PcInstance?(L2PcInstance)killer:((L2SummonInstance)killer).getOwner(); // Get a table containing all QuestState to modify after the L2Attackable killing QuestState[] quests = player.getQuestsForKills(this); if (quests != null) { // Go through table containing all QuestState to modify for (QuestState qs : quests) { // Notify the Quest Engine of the L2Attackable death qs.getQuest().notifyKill(this, qs); } } } } catch (Exception e) { _log.log(Level.SEVERE, "", e); } // Kill the L2NpcInstance (the corpse disappeared after 7 seconds) super.doDie(killer); } /** * Distribute Exp and SP rewards to L2PcInstance (including Summon owner) that hit the L2Attackable and to their Party members.

    * * Actions :

    *
  • Get the L2PcInstance owner of the L2SummonInstance (if necessary) and L2Party in progress
  • *
  • Calculate the Experience and SP rewards in function of the level difference
  • *
  • Add Exp and SP rewards to L2PcInstance (including Summon penalty) and to Party members in the known area of the last attacker


  • * * Caution : This method DOESN'T GIVE rewards to L2PetInstance

    * * @param lastAttacker The L2Character that has killed the L2Attackable * */ private void calculateRewards(L2Character lastAttacker) { if (_aggroList == null) return; // Get the Identifier of the L2Attackable killed int npcID = getTemplate().npcId; // Creates an empty list of rewards FastMap rewards = new FastMap().setShared(true); int rewardCount = 0; synchronized (_aggroList) { // Go through the _aggroList of the L2Attackable if (!_aggroList.isEmpty()) { List toRemove = new FastList(); for (AggroInfo info : _aggroList.values()) { // Get the L2Character corresponding to this attacker if (info == null) continue; L2Character attacker = info.attacker; L2Character ddealer = info.attacker; // Get damages done by this attacker int damage = info.damage; // Prevent unwanted behavior if (npcID <= 0 || damage <= 1) { toRemove.add(attacker); //_aggroList.remove(attacker); continue; } if (attacker instanceof L2SummonInstance) ddealer = ((L2SummonInstance)attacker).getOwner(); // Calculate real damages (Summoners should get own damage plus summon's damage) RewardInfo reward = null; reward = rewards.get(ddealer); if (reward == null) { reward = new RewardInfo(ddealer, damage); rewardCount++; } else { reward.addDamage(damage); } rewards.put(ddealer, reward); /* if (rewards.isEmpty() || !rewards.containsKey(ddealer)) { reward = new RewardInfo(ddealer, damage); rewards.put(ddealer, reward); } else { reward = rewards.get(ddealer); reward.addDamage(damage); }*/ //_aggroList.remove(attacker); toRemove.add(attacker); } for (L2Character attacker : toRemove) { if (attacker != null) _aggroList.remove(attacker); } } } if (!rewards.isEmpty()) { for (FastMap.Entry entry = rewards.head(), end = rewards.tail(); (entry = entry.getNext()) != end;) { if (entry == null) continue; RewardInfo reward = entry.getValue(); if(reward == null) continue; // Penalty applied to the attacker's XP float penalty = 0; // Attacker to be rewarded L2Character attacker = reward.attacker; // Total amount of damage done int damage = reward.dmg; // Create a new L2Party object L2Party attackerParty = null; // If the attacker is a Pet, get the party of the owner if (attacker instanceof L2PetInstance) attackerParty = ((L2PetInstance)attacker).getParty(); else if (!(attacker instanceof L2PcInstance)) return; else attackerParty = ((L2PcInstance)attacker).getParty(); // If this attacker is a L2PcInstance with a summoned L2SummonInstance, get Exp Penalty applied for the current summoned L2SummonInstance if (attacker instanceof L2PcInstance && ((L2PcInstance)attacker).getPet() instanceof L2SummonInstance) { L2SummonInstance summon = (L2SummonInstance)((L2PcInstance)attacker).getPet(); penalty = summon.getExpPenalty(); } // We must avoid "over damage", if any if (damage > getMaxHp()) damage = getMaxHp(); // If there's NO party in progress if (attackerParty == null) { // Calculate the difference of level between this attacker (L2PcInstance or L2SummonInstance owner) and the L2Attackable // mob = 24, atk = 10, diff = -14 (full xp) // mob = 24, atk = 28, diff = 4 (some xp) // mob = 24, atk = 50, diff = 26 (no xp) int diff = attacker.getLevel() - getLevel(); int xp = 0; int sp = 0; // Calculate Exp and SP rewards if (attacker.getKnownList().knowsObject(this)) { int[] tmp = calculateExpAndSp(diff, damage); xp = tmp[0]; xp *= 1 - penalty; sp = tmp[1]; } // Distribute the Exp and SP between the L2PcInstance and its L2Summon attacker.addExpAndSp((int)attacker.calcStat(Stats.EXPSP_RATE, xp, null, null), (int)attacker.calcStat(Stats.EXPSP_RATE, sp, null, null)); } else { //share with party members int partyDmg = 0; float partyMul = 1.f; // Get all L2Character (including L2Summon) that can be rewarded in the party List rewardedMembers = new FastList(); // Go through all L2PcInstance in the party for (L2PcInstance pl : attackerParty.getPartyMembers()) { if (pl == null) continue; // Get the RewardInfo of this L2PcInstance from L2Attackable rewards RewardInfo ai = rewards.get(pl); // If the L2PcInstance is in the L2Attackable rewards add its damages to party damages if (ai != null) { // Add L2PcInstance damages to party damages partyDmg += ai.dmg; // Remove the L2PcInstance from the L2Attackable rewards rewards.remove(pl); } // Add L2PcInstance of the party (that have attacked or not) to members that can be rewarded if it's not dead if (!pl.isDead()) rewardedMembers.add(pl); } // If the party didn't killed this L2Attackable alone if (partyDmg < getMaxHp()) { float partyDmgF = partyDmg; float getMaxHpf = getMaxHp(); partyMul = (partyDmgF / getMaxHpf); } // Avoid "over damage" if (partyDmg > getMaxHp()) partyDmg = getMaxHp(); // Calculate the level difference between Party and L2Attackable int diff = attackerParty.getLevel() - getLevel(); // Calculate Exp and SP rewards int[] tmp = calculateExpAndSp(diff, partyDmg); int xp = tmp[0]; xp *= 1 - penalty; int sp = tmp[1]; xp *= partyMul; sp *= partyMul; // Distribute Experience and SP rewards to L2PcInstance Party members in the known area of the last attacker if (partyDmg > 0) attackerParty.distributeXpAndSp(xp, sp, rewardedMembers, lastAttacker); } } } _aggroList = null; rewards = null; // Manage Base, Quests and Special Events drops of the L2Attackable doItemDrop(lastAttacker); // Manage Sweep drops of the L2Attackable if (isSpoil()) doSweepDrop(lastAttacker); } /** * Add damage and hate to the attacker AggroInfo of the L2Attackable _aggroList.

    * * @param attacker The L2Character that gave damages to this L2Attackable * @param damage The number of damages given by the attacker L2Character * */ public void addDamage(L2Character attacker, int damage) { addDamageHate(attacker, damage, damage); } /** * Add damage and hate to the attacker AggroInfo of the L2Attackable _aggroList.

    * * @param attacker The L2Character that gave damages to this L2Attackable * @param damage The number of damages given by the attacker L2Character * @param aggro The hate (=damage) given by the attacker L2Character * */ public void addDamageHate(L2Character attacker, int damage, int aggro) { if (attacker == null) return; if (_aggroList == null) { // If necessary, create the _aggroList _aggroList = Collections.synchronizedMap(new WeakHashMap()); // Create a AggroInfo object and Init it AggroInfo ai = new AggroInfo(attacker); ai.damage = damage; ai.hate = aggro; // Add the attaker L2Character and the AggroInfo object in the _aggroList of the L2Attackable _aggroList.put(attacker, ai); } else { // Get the AggroInfo of the attacker L2Character from the _aggroList of the L2Attackable AggroInfo ai = _aggroList.get(attacker); if (ai != null) { // Add new damage and aggro (=damage) to the AggroInfo object ai.damage += damage; ai.hate += aggro; } else { // Create a AggroInfo object and Init it ai = new AggroInfo(attacker); ai.damage = damage; ai.hate = aggro; // Add the attaker L2Character and the AggroInfo object in the _aggroList of the L2Attackable if (_aggroList != null) _aggroList.put(attacker, ai); } } // Set the intention to the L2Attackable to AI_INTENTION_ACTIVE if (aggro > 0 && getAI().getIntention() == CtrlIntention.AI_INTENTION_IDLE) getAI().setIntention(CtrlIntention.AI_INTENTION_ACTIVE); // Notify the L2Attackable AI with EVT_ATTACKED if (damage > 0) getAI().notifyEvent(CtrlEvent.EVT_ATTACKED, attacker); try { if (attacker instanceof L2PcInstance || attacker instanceof L2SummonInstance) { L2PcInstance player = attacker instanceof L2PcInstance?(L2PcInstance)attacker:((L2SummonInstance)attacker).getOwner(); QuestState[] quests = player.getQuestsForAttacks(this); if (quests != null) { for (QuestState qs : quests) qs.getQuest().notifyAttack(this, qs); } } } catch (Exception e) { _log.log(Level.SEVERE, "", e); } } /** * Return the most hated L2Character of the L2Attackable _aggroList.

    */ public L2Character getMostHated() { if (_aggroList == null || isAlikeDead()) return null; L2Character mostHated = null; int maxHate = 0; // Go through the aggroList of the L2Attackable synchronized(_aggroList) { if (_aggroList == null || _aggroList.size() == 0) return null; for (AggroInfo ai : _aggroList.values()) { if (ai == null) continue; if (ai.attacker.isAlikeDead() || !getKnownList().knowsObject(ai.attacker) ||!ai.attacker.isVisible()) ai.hate = 0; if (ai.hate > maxHate) { mostHated = ai.attacker; maxHate = ai.hate; } } } return mostHated; } /** * Return the hate level of the L2Attackable against this L2Character contained in _aggroList.

    * * @param target The L2Character whose hate level must be returned * */ public int getHating(L2Character target) { if (_aggroList == null) return 0; AggroInfo ai = _aggroList.get(target); if (ai == null) return 0; if (!ai.attacker.isVisible()) { _aggroList.remove(target); return 0; } if (ai.attacker.isAlikeDead()) { ai.hate = 0; return 0; } return ai.hate; } /** * Calculates quantity of items for specific drop acording to current situation
    * * @param drop The L2DropData count is being calculated for * @param lastAttacker The L2PcInstance that has killed the L2Attackable * @param deepBlueDrop Factor to divide the drop chance * @param levelModifier level modifier in %'s (will be subtracted from drop chance) */ private int calculateDropCount(L2PcInstance lastAttacker, L2DropData drop, int levelModifier) { // Roll the dice.. between 0 and MAX_CHANCE (=1000000) int roll = Rnd.get(1, L2DropData.MAX_CHANCE); float rate = (drop.isSweep()? Config.RATE_DROP_SPOIL:Config.RATE_DROP_ITEMS); // Get default drop chance float dropChance = drop.getChance() * rate; int deepBlueDrop = 1; if (Config.DEEPBLUE_DROP_RULES) { if (levelModifier > 0) { // We should multiply by the server's drop rate, so we always get a low chance of drop for deep blue mobs. // NOTE: This is valid only for adena drops! Others drops will still obey server's rate deepBlueDrop = 3; if (drop.getItemId() == 57) deepBlueDrop *= (int)Config.RATE_DROP_ITEMS; } } // Check if we should apply our maths so deep blue mobs will not drop that easy if (Config.DEEPBLUE_DROP_RULES) dropChance = ((drop.getChance() - ((drop.getChance() * levelModifier)/100)) * rate / deepBlueDrop); // Round drop chance dropChance = Math.round(dropChance); // Set our limits for chance of drop if (dropChance < 1) dropChance = 1; if (drop.getItemId() == 57 && dropChance > L2DropData.MAX_CHANCE) dropChance = L2DropData.MAX_CHANCE; // If item is adena, dont drop multiple time int itemCount = 0; // Check if the Item must be dropped while (roll <= dropChance) { // Get min and max Item quantity that can be dropped in one time int min = drop.getMinDrop(); int max = drop.getMaxDrop(); // Get the item quantity dropped if (min < max) itemCount += Rnd.get(min, max); else if (min == max) itemCount += min; else itemCount++; // Calculate the Adena reward with the RATE_DROP_ADENA if (drop.getItemId() == 57) itemCount *= Config.RATE_DROP_ADENA; // Prepare for next iteration if dropChance > L2DropData.MAX_CHANCE dropChance -= L2DropData.MAX_CHANCE; } if (itemCount == 0 && Config.DEBUG) _log.fine("Roll produced 0 items to drop..."); return itemCount; } /** * Calculates the level modifier for drop
    * * @param lastAttacker The L2PcInstance that has killed the L2Attackable */ private int calculateLevelModifierForDrop(L2PcInstance lastAttacker) { if (Config.DEEPBLUE_DROP_RULES) { int highestLevel = lastAttacker.getLevel(); // Check to prevent very high level player to nearly kill mob and let low level player do the last hit. if (getAttackByList() != null && getAttackByList().size() > 0) { for (L2Character atkChar: getAttackByList()) if (atkChar != null && atkChar.getLevel() > highestLevel) highestLevel = atkChar.getLevel(); } // According to official data (Prima), deep blue mobs are 9 or more levels below players if (highestLevel - 9 >= getLevel()) return ((highestLevel - (getLevel() + 8)) * 9); } return 0; } private void dropItem(L2PcInstance lastAttacker, int itemId, int itemCount) { L2ItemInstance dropit; _rand = new Random(); for (int i = 1; i <= itemCount; i++) { dropit = ItemTable.getInstance().createItem(itemId); // Set the Item quantity dropped if L2ItemInstance is stackable if (dropit.isStackable()) { i = itemCount; // Set so loop won't happent again dropit.setCount(itemCount); // Set item count } if (Config.DEBUG) _log.fine("Item id to drop: " + itemId + " amount: " + dropit.getCount()); if(this instanceof L2RaidBossInstance) { SystemMessage sm = new SystemMessage(SystemMessage.S1_DIED_DROPPED_S3_S2); sm.addString(getName()); sm.addNumber(dropit.getCount()); sm.addItemName(itemId); broadcastPacket(sm); } // Check if the autoLoot mode is active if (Config.AUTO_LOOT) lastAttacker.doAutoLoot(dropit); // Give this or these Item(s) to the L2PcInstance that has killed the L2Attackable else { int dropConstant; int randDropLim = 70; int randPlusMin = 1; dropConstant = _rand.nextInt(randDropLim); //randomize +/- randPlusMin = _rand.nextInt(2); if ( randPlusMin == 1 ) dropConstant *= -1; int newX = this.getX() + (int)Math.round(dropConstant); dropConstant = _rand.nextInt(randDropLim); //randomize +/- randPlusMin = _rand.nextInt(2); if ( randPlusMin == 1 ) dropConstant *= -1; int newY = this.getY() + (int)Math.round(dropConstant); // Init the dropped L2ItemInstance and add it in the world as a visible object at the position where mob was last dropit.dropMe(this, newX, newY, this.getZ()); // Add drop to auto destroy item task if (Config.AUTODESTROY_ITEM_AFTER > 0) ItemsAutoDestroy.getInstance().addItem(dropit); } } } /** * Manage Special Events drops created by GM for a defined period.

    * * Concept :

    * During a Special Event all L2Attackable can drop extra Items. * Those extra Items are defined in the table allNpcDateDrops of the EventDroplist. * Each Special Event has a start and end date to stop to drop extra Items automaticaly.

    * * Actions : If an extra drop must be generated

    *
  • Get an Item Identifier (random) from the DateDrop Item table of this Event
  • *
  • Get the Item quantity dropped (random)
  • *
  • Create this or these L2ItemInstance corresponding to this Item Identifier
  • *
  • If the autoLoot mode is actif and if the L2Character that has killed the L2Attackable is a L2PcInstance, give this or these Item(s) to the L2PcInstance that has killed the L2Attackable
  • *
  • If the autoLoot mode isn't actif or if the L2Character that has killed the L2Attackable is not a L2PcInstance, add this or these Item(s) in the world as a visible object at the position where mob was last


  • * * @param lastAttacker The L2Character that has killed the L2Attackable */ public void doEventDrop(L2Character lastAttacker) { L2PcInstance player = null; if (lastAttacker instanceof L2PcInstance) player = (L2PcInstance)lastAttacker; else if (lastAttacker instanceof L2Summon) player = ((L2Summon)lastAttacker).getOwner(); if (player == null) return; // Don't drop anything if the last attacker or ownere isn't L2PcInstance if (player.getLevel() - getLevel() > 9) return; // Go through DateDrop of EventDroplist allNpcDateDrops within the date range int itemCount; int max; int min; for (DateDrop drop : EventDroplist.getInstance().getAllDrops()) { if (Rnd.get(1, L2DropData.MAX_CHANCE) < drop.chance) { // Get min and max Item quantity that can be dropped in one time max = drop.max; min = drop.min; // Get the item quantity dropped itemCount = 0; if (min < max) itemCount += Rnd.get(min, max); else if (min == max) itemCount += min; else itemCount++; dropItem(player, drop.items[Rnd.get(0, drop.items.length - 1)], itemCount); } } } /** * Manage Base, Quests and Special Events drops of L2Attackable (called by calculateRewards).

    * * Concept :

    * During a Special Event all L2Attackable can drop extra Items. * Those extra Items are defined in the table allNpcDateDrops of the EventDroplist. * Each Special Event has a start and end date to stop to drop extra Items automaticaly.

    * * Actions :

    *
  • Manage drop of Special Events created by GM for a defined period
  • *
  • Get all possible drops of this L2Attackable from L2NpcTemplate and add it Quest drops
  • *
  • For each possible drops (base + quests), calculate which one must be dropped (random)
  • *
  • Get each Item quantity dropped (random)
  • *
  • Create this or these L2ItemInstance corresponding to each Item Identifier dropped
  • *
  • If the autoLoot mode is actif and if the L2Character that has killed the L2Attackable is a L2PcInstance, give this or these Item(s) to the L2PcInstance that has killed the L2Attackable
  • *
  • If the autoLoot mode isn't actif or if the L2Character that has killed the L2Attackable is not a L2PcInstance, add this or these Item(s) in the world as a visible object at the position where mob was last


  • * * @param lastAttacker The L2Character that has killed the L2Attackable */ public void doItemDrop(L2Character lastAttacker) { L2PcInstance player = null; if (lastAttacker instanceof L2PcInstance) player = (L2PcInstance)lastAttacker; else if (lastAttacker instanceof L2Summon) player = ((L2Summon)lastAttacker).getOwner(); if (player == null) return; // Don't drop anything if the last attacker or ownere isn't L2PcInstance int levelModifier = calculateLevelModifierForDrop(player);; // level modifier in %'s (will be subtracted from drop chance) // Manage drop of Special Events created by GM for a defined period doEventDrop(lastAttacker); // Create a table containing all possible drops of this L2Attackable from L2NpcTemplate List drops = new FastList(); drops.addAll(getTemplate().getDropData()); if (Config.DEBUG) _log.finer("this npc has "+drops.size()+" drops defined"); // Add Quest drops to the table containing all possible drops of this L2Attackable player.fillQuestDrops(this, drops); // Go Throw all possible drops (base + quests) of this L2Attackable for (L2DropData drop : drops) { if (drop == null) continue; // If it's a sweep drop, it will not appear on the ground if (drop.isSweep()) continue; dropItem(player, drop.getItemId(), calculateDropCount(player, drop, levelModifier)); } } /** * Manage Sweep drops of L2Attackable.

    * * Actions :

    *
  • Get all possible drops of this L2Attackable from L2NpcTemplate
  • *
  • For each possible drops (base including sweep), calculate which one can is spoiled (random)
  • *
  • Get each Item spoiled quantity (random)
  • *
  • Create this or these L2ItemInstance corresponding to each Item Identifier spoiled
  • *
  • Add each of those L2ItemInstance to the table _sweepItems of this L2Attackable containing all Items that can be spoiled


  • * * @param lastAttacker The L2Character that has killed the L2Attackable */ public void doSweepDrop(L2Character lastAttacker) { L2PcInstance player = null; if (lastAttacker instanceof L2PcInstance) player = (L2PcInstance)lastAttacker; else if (lastAttacker instanceof L2Summon) player = ((L2Summon)lastAttacker).getOwner(); if (player == null) return; // Don't drop anything if the last attacker or ownere isn't L2PcInstance int levelModifier = calculateLevelModifierForDrop(player);; // level modifier in %'s (will be subtracted from drop chance) // Create a table containing all possible drops of this L2Attackable from L2NpcTemplate List drops = getTemplate().getDropData(); if (Config.DEBUG) _log.finer("this npc has "+drops.size()+" drops defined"); // Create a table that will contain all Items that can be spoiled List spoiled = new FastList(); // Go Throw all possible drops (base including sweep) of this L2Attackable L2ItemInstance dropit; int itemCount; for (L2DropData drop: drops) { if (drop == null) continue; // If it's not a sweep drop, it will not appear on the ground only drop adena if seeded with low-grade seed if (!drop.isSweep() || (isSeeded() && !isSeededHigh() && drop.getItemId() != 57)) continue; // Calculates Item drop count with current rates... itemCount = calculateDropCount(player, drop, levelModifier); for (int i = 1; i <= itemCount; i++) { dropit = ItemTable.getInstance().createItem(drop.getItemId()); // Set the Item quantity dropped if L2ItemInstance is stackable if (dropit.isStackable()) { i = itemCount; // Set so loop won't happent again dropit.setCount(itemCount); // Set item count } spoiled.add(dropit); if (Config.DEBUG) _log.fine("Item id to spoil: " + drop.getItemId() + " amount: " + dropit.getCount()); } } // Set the table _sweepItems of this L2Attackable if (spoiled.size() > 0) _sweepItems = spoiled.toArray(new L2ItemInstance[spoiled.size()]); } /** * Return the active weapon of this L2Attackable (= null).

    */ public L2ItemInstance getActiveWeapon() { return null; } /** * Return True if the _aggroList of this L2Attackable is Empty.

    */ public boolean noTarget() { return _aggroList==null || _aggroList.isEmpty(); } /** * Return True if the _aggroList of this L2Attackable contains the L2Character.

    * * @param player The L2Character searched in the _aggroList of the L2Attackable * */ public boolean containsTarget(L2Character player) { if (_aggroList == null) return false; return _aggroList.containsKey(player); } /** * Clear the _aggroList of the L2Attackable.

    */ public void clearAggroList() { _aggroList = null; } /** * Return True if a Dwarf use Sweep on the L2Attackable and if item can be spoiled.

    */ public boolean isSweepActive() { return _sweepItems != null; } /** * Return table containing all L2ItemInstance that can be spoiled.

    */ public synchronized L2ItemInstance[] takeSweep() { L2ItemInstance[] sweep = _sweepItems; _sweepItems = null; return sweep; } /** * Activate the absorbed soul condition on the L2Attackable.

    */ public void absorbSoul() { _absorbed = true; } /** * Return True if the L2Attackable had his soul absorbed.

    */ public boolean isAbsorbed() { return _absorbed; } /** * Adds an attacker that successfully absorbed the soul of this L2Attackable into the _absorbersList.

    * * params: attacker - a valid L2PcInstance * condition - an integer indicating the event when mob dies. This should be: * = 0 - "the crystal scatters"; * = 1 - "the crystal failed to absorb. nothing happens"; * = 2 - "the crystal resonates because you got more than 1 crystal on you"; * = 3 - "the crystal cannot absorb the soul because the mob level is too low"; * = 4 - "the crystal successfuly absorbed the soul"; */ public void addAbsorber(L2PcInstance attacker, int crystalId) { // This just works for targets like L2MonsterInstance if (!(this instanceof L2MonsterInstance)) return; // The attacker must not be null if (attacker == null) return; // This L2Attackable must be of one type in the _absorbingMOBS_levelXX tables. // OBS: This is done so to avoid triggering the absorbed conditions for mobs that can't be absorbed. boolean validTarget = false; for (int mobId : _absorbingMOBS_level4) { if (getNpcId() == mobId) { validTarget = true; break; } } if (!validTarget) { for (int mobId : _absorbingMOBS_level8) { if (getNpcId() == mobId) { validTarget = true; break; } } } if (!validTarget) { for (int mobId : _absorbingMOBS_level10) { if (getNpcId() == mobId) { validTarget = true; break; } } } if (!validTarget) { for (int mobId : _absorbingMOBS_level12) { if (getNpcId() == mobId) { validTarget = true; break; } } } if (!validTarget) return; // If we have no _absorbersList initiated, do it AbsorberInfo ai = null; if (_absorbersList == null) { _absorbersList = Collections.synchronizedMap(new WeakHashMap()); } else { ai = _absorbersList.get(attacker); } // If the L2Character attacker isn't already in the _absorbersList of this L2Attackable, add it if (ai == null) { ai = new AbsorberInfo(attacker, crystalId, getCurrentHp()); _absorbersList.put(attacker, ai); } else { ai.absorber = attacker; ai.crystalId = crystalId; ai.absorbedHP = getCurrentHp(); } // Set this L2Attackable as absorbed absorbSoul(); } /** * Calculate the leveling chance of Soul Crystals based on the attacker that killed this L2Attackable * * @param attacker The player that last hitted (killed) this L2Attackable * */ private void levelSoulCrystals(L2Character attacker) { // Only L2PcInstance can absorb a soul if (!(attacker instanceof L2PcInstance)) { _absorbed = false; _absorbersList = null; return; } // Init some useful vars boolean success = false; boolean levelPartyCrystals = false; boolean canIncreaseCrystal = true; int thisMobId = getNpcId(); L2PcInstance killer = (L2PcInstance)attacker; // Check if this L2Attackable isn't within any of the groups of mobs that can be absorbed int minCrystalLevel = 0; int maxCrystalLevel = 0; for (int mobId : _absorbingMOBS_level4) { if (thisMobId == mobId) { maxCrystalLevel = 4; success = true; break; } } if (!success) { for (int mobId : _absorbingMOBS_level8) { if (thisMobId == mobId) { maxCrystalLevel = 8; success = true; break; } } } if (!success) { for (int mobId : _absorbingMOBS_level10) { if (thisMobId == mobId) { maxCrystalLevel = 10; success = true; break; } } } if (!success) { for (int mobId : _absorbingMOBS_level12) { if (thisMobId == mobId) { minCrystalLevel = 10; maxCrystalLevel = 12; success = true; levelPartyCrystals = true; break; } } } // If this is not a valid L2Attackable, clears the _absorbersList and just return if (!success) { _absorbed = false; _absorbersList = null; return; } // If this mob is a boss, then skip some checkings success = true; if (!levelPartyCrystals) { // Fail if this L2Attackable isn't absorbed or there's no one in its _absorbersList if (!isAbsorbed() || _absorbersList == null) { _absorbed = false; _absorbersList = null; return; } // Fail if the killer isn't in the _absorbersList of this L2Attackable and mob is not boss AbsorberInfo ai = _absorbersList.get(killer); if (ai == null || ai.absorber.getObjectId() != killer.getObjectId()) success = false; // Check if the soul crystal was used when HP of this L2Attackable wasn't higher than half of it if (ai != null && ai.absorbedHP > (getMaxHp()/2)) success = false; if (!success) { _absorbed = false; _absorbersList = null; return; } } // Init some more info now Random _rnd = new Random(); int dice = _rnd.nextInt(100); int crystalId = 0; int oldCrystalId = 0; int newCrystalId = 0; int crystalOffset = 0; int crystalsCount = 0; SystemMessage sm = new SystemMessage(614); success = false; // Now we got four choices: // 1- The Monster level is too low for the crystal. Nothing happens. // 2- Everything is correct, but it failed. Nothing happens. (57.5%) // 3- Everything is correct, but it failed. The crystal scatters. A sound event is played. (10%) // 4- Everything is correct, the crystal level up. A sound event is played. (32.5%) List players = new FastList(); if (levelPartyCrystals && killer.isInParty()) { players = killer.getParty().getPartyMembers(); } else { players.add(killer); } for (L2PcInstance player : players) { // Check how many soul crystals the player has in his inventory and which soul crystal he has canIncreaseCrystal = true; success = true; final L2ItemInstance[] inv = player.getInventory().getItems(); for (int index = 0; index < inv.length; index++) { final L2ItemInstance item = inv[index]; if ( (((item.getItemId() >= _REDCRYSTAL_OFFSET) || (item.getItemId() >= _GREENCRYSTAL_OFFSET) || (item.getItemId() >= _BLUECRYSTAL_OFFSET)) && ((item.getItemId() <= _REDCRYSTAL_OFFSET+10) || (item.getItemId() <= _GREENCRYSTAL_OFFSET+10) || (item.getItemId() <= _BLUECRYSTAL_OFFSET+10))) || (item.getItemId() == _REDCRYSTAL_LVL11) || (item.getItemId() == _BLUECRYSTAL_LVL11) || (item.getItemId() == _GREENCRYSTAL_LVL11) || (item.getItemId() == _REDCRYSTAL_LVL12) || (item.getItemId() == _BLUECRYSTAL_LVL12) || (item.getItemId() == _GREENCRYSTAL_LVL12) ) { crystalsCount++; if (crystalsCount > 1) { success = false; break; // break the "findCrystals" loop, as we already found more than one crystal. } crystalId = item.getItemId(); crystalOffset = 0; for (int crystalLevel = 0; crystalLevel <= _MAX_CRYSTALS_LEVEL; crystalLevel++) { // Get a crystal offset to calculate the new crystal ID if (crystalId == (_REDCRYSTAL_OFFSET + crystalLevel)) crystalOffset = _REDCRYSTAL_OFFSET; if (crystalId == (_GREENCRYSTAL_OFFSET + crystalLevel)) crystalOffset = _GREENCRYSTAL_OFFSET; if (crystalId == (_BLUECRYSTAL_OFFSET + crystalLevel)) crystalOffset = _BLUECRYSTAL_OFFSET; if (crystalLevel > 9) { if (crystalId == (_REDCRYSTAL_OFFSET+10)) crystalOffset = _REDCRYSTAL_LVL11 - (crystalLevel + 1); if (crystalId == (_GREENCRYSTAL_OFFSET+10)) crystalOffset = _GREENCRYSTAL_LVL11 - (crystalLevel + 1); if (crystalId == (_BLUECRYSTAL_OFFSET+10)) crystalOffset = _BLUECRYSTAL_LVL11 - (crystalLevel + 1); if (crystalId == (_REDCRYSTAL_LVL11)) crystalOffset = _REDCRYSTAL_LVL12 - (crystalLevel + 1); if (crystalId == (_GREENCRYSTAL_LVL11)) crystalOffset = _GREENCRYSTAL_LVL12 - (crystalLevel + 1); if (crystalId == (_BLUECRYSTAL_LVL11)) crystalOffset = _BLUECRYSTAL_LVL12 - (crystalLevel + 1); } // If the crystal level is way too high for this mob, say that we can't increase it if ((crystalLevel >= minCrystalLevel) && (crystalLevel < maxCrystalLevel)) { canIncreaseCrystal = true; } else { canIncreaseCrystal = false; } // Define crystals' IDs based on the crystalOffset found if (crystalOffset > 0) { oldCrystalId = crystalId; newCrystalId = (crystalOffset + crystalLevel) + 1; break; // Break the "findLevel" loop, as we already got the crystal Level. } } } } // The player has no crystals at all, reset our control vars and skip this player if (crystalsCount < 1) { oldCrystalId = 0; newCrystalId = 0; crystalOffset = 0; crystalsCount = 0; continue; // The player doesn't have any crystals with him. Get to the next player. } // The player has more than one soul crystal with him, the crystal resonates and we skip this player if (!success) { sm.addString("The soul crystal resonated."); player.sendPacket(new PlaySound("ItemSound2.broken_key")); player.sendPacket(sm); oldCrystalId = 0; newCrystalId = 0; crystalOffset = 0; crystalsCount = 0; continue; // The player failed to increase his crystal as he has more than one crystal. } // The soul crystal stage of the player is way too high, refuse to increase it if (!canIncreaseCrystal) { sm.addString("The soul crystal has refused to absorb the soul."); player.sendPacket(sm); oldCrystalId = 0; newCrystalId = 0; crystalOffset = 0; crystalsCount = 0; continue; // The player failed to increase his crystal cuz mob is wrong for his crystal level. } // If the killer succeeds or it is a boss mob, level up the crystal if ((dice < _LEVEL_CHANCE) || levelPartyCrystals) { final L2ItemInstance oldCrystal = player.getInventory().getItemByItemId(oldCrystalId); final L2ItemInstance newCrystal = ItemTable.getInstance().createItem(newCrystalId); final SystemMessage sms = new SystemMessage(SystemMessage.EARNED_ITEM); // Add new crystal to the killer's inventory player.getInventory().addItem(newCrystal); // Remove old crystal from the killer's inventory player.removeItemFromInventory(oldCrystal, 1); // Send a sound event and text message to the player player.sendPacket(new PlaySound("ItemSound.quest_itemget")); sms.addItemName(newCrystalId); player.sendPacket(sms); // Send Server->Client packets to notify the killer and update his Inventory player.sendPacket(new ItemList(player, false)); } else if ((dice >= 100.0 - _BREAK_CHANCE) && !levelPartyCrystals) { // Remove old crystal from the killer's inventory final L2ItemInstance oldCrystal = player.getInventory().getItemByItemId(oldCrystalId); killer.removeItemFromInventory(oldCrystal, 1); killer.sendPacket(new PlaySound("ItemSound3.sys_enchant_failed")); sm.addString("The soul crystal scattered itself."); killer.sendPacket(sm); _absorbed = false; _absorbersList = null; return; } else { sm.addString("The soul crystal has failed to absorb the soul."); player.sendPacket(sm); } } } /** * Calculate the Experience and SP to distribute to attacker (L2PcInstance, L2SummonInstance or L2Party) of the L2Attackable.

    * * @param diff The difference of level between attacker (L2PcInstance, L2SummonInstance or L2Party) and the L2Attackable * @param damage The damages given by the attacker (L2PcInstance, L2SummonInstance or L2Party) * */ private int[] calculateExpAndSp(int diff, int damage) { int xp; int sp; if (Config.ALT_GAME_EXPONENT_XP != 0) xp = (int)((double)getExpReward() * damage / getMaxHp() * Math.pow(2., -diff / Config.ALT_GAME_EXPONENT_XP)); else xp = getExpReward() * damage / getMaxHp(); if (Config.ALT_GAME_EXPONENT_SP != 0) sp = (int)((double)getSpReward() * damage / getMaxHp() * Math.pow(2., -diff / Config.ALT_GAME_EXPONENT_SP)); else sp = getSpReward() * damage / getMaxHp(); if (Config.ALT_GAME_EXPONENT_XP == 0 && Config.ALT_GAME_EXPONENT_SP == 0) { // deep blue mob, is more than 8 levels below attacker lvl if (diff > 8) { xp = 0; sp = 0; } // green or light blue mob, is 6 to 8 levels below attacker lvl else if (diff > 5) { xp -= diff * xp / 10; sp -= diff * sp / 10; } if (xp <= 0) { xp = 0; sp = 0; } else if (sp <= 0) { sp = 0; } } int[] tmp = { (int)xp, (int)sp }; return tmp; } /** * Return True.

    */ public boolean isAttackable() { return true; } @Override public void OnSpawn() { // Clear mob spoil,seed setSpoil(false); _sweepItems = null; _absorbed = false; _absorbersList = null; } public synchronized L2ItemInstance[] takeHarvest() { L2ItemInstance[] harvest = _harvestItems; _harvestItems = null; return harvest; } public void setSeeded(int id, int seederLvl) { _seeded = true; _seedType = id; int count = 1; int diff = (getLevel() - seederLvl - 1); // hi-lvl mobs bonus if (diff > 0 && diff < 5)//Config.MANOR_HARVEST_DIFF_BONUS) { Random rnd = new Random(); count += rnd.nextInt(diff); } List harvested = new FastList(); for (int i = 0; i < count; i++) { harvested.add(ItemTable.getInstance().createItem( L2Manor.getInstance().getCropType(_seedType))); } _harvestItems = harvested.toArray(new L2ItemInstance[harvested.size()]); } public boolean isSeeded() { return _seeded; } private boolean isSeededHigh() { return (_seedType < 5650); // low-grade seeds has id's below 5650 } }