/*
* Copyright (C) 2004-2015 L2J Server
*
* This file is part of L2J Server.
*
* L2J Server 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.
*
* L2J Server is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see .
*/
package com.l2jserver.gameserver.model.actor;
import java.util.ArrayList;
import java.util.Collection;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.atomic.AtomicReference;
import java.util.logging.Level;
import com.l2jserver.Config;
import com.l2jserver.gameserver.ThreadPoolManager;
import com.l2jserver.gameserver.ai.CtrlEvent;
import com.l2jserver.gameserver.ai.CtrlIntention;
import com.l2jserver.gameserver.ai.L2AttackableAI;
import com.l2jserver.gameserver.ai.L2CharacterAI;
import com.l2jserver.gameserver.ai.L2FortSiegeGuardAI;
import com.l2jserver.gameserver.ai.L2SiegeGuardAI;
import com.l2jserver.gameserver.datatables.EventDroplist;
import com.l2jserver.gameserver.datatables.EventDroplist.DateDrop;
import com.l2jserver.gameserver.datatables.ItemTable;
import com.l2jserver.gameserver.enums.InstanceType;
import com.l2jserver.gameserver.instancemanager.CursedWeaponsManager;
import com.l2jserver.gameserver.instancemanager.WalkingManager;
import com.l2jserver.gameserver.model.AbsorberInfo;
import com.l2jserver.gameserver.model.AggroInfo;
import com.l2jserver.gameserver.model.DamageDoneInfo;
import com.l2jserver.gameserver.model.L2CommandChannel;
import com.l2jserver.gameserver.model.L2Object;
import com.l2jserver.gameserver.model.L2Party;
import com.l2jserver.gameserver.model.L2Seed;
import com.l2jserver.gameserver.model.actor.instance.L2GrandBossInstance;
import com.l2jserver.gameserver.model.actor.instance.L2MonsterInstance;
import com.l2jserver.gameserver.model.actor.instance.L2PcInstance;
import com.l2jserver.gameserver.model.actor.instance.L2ServitorInstance;
import com.l2jserver.gameserver.model.actor.knownlist.AttackableKnownList;
import com.l2jserver.gameserver.model.actor.status.AttackableStatus;
import com.l2jserver.gameserver.model.actor.tasks.attackable.CommandChannelTimer;
import com.l2jserver.gameserver.model.actor.templates.L2NpcTemplate;
import com.l2jserver.gameserver.model.drops.DropListScope;
import com.l2jserver.gameserver.model.events.EventDispatcher;
import com.l2jserver.gameserver.model.events.impl.character.npc.attackable.OnAttackableAggroRangeEnter;
import com.l2jserver.gameserver.model.events.impl.character.npc.attackable.OnAttackableAttack;
import com.l2jserver.gameserver.model.events.impl.character.npc.attackable.OnAttackableKill;
import com.l2jserver.gameserver.model.holders.ItemHolder;
import com.l2jserver.gameserver.model.items.L2Item;
import com.l2jserver.gameserver.model.items.instance.L2ItemInstance;
import com.l2jserver.gameserver.model.skills.Skill;
import com.l2jserver.gameserver.model.stats.Stats;
import com.l2jserver.gameserver.network.SystemMessageId;
import com.l2jserver.gameserver.network.clientpackets.Say2;
import com.l2jserver.gameserver.network.serverpackets.CreatureSay;
import com.l2jserver.gameserver.network.serverpackets.SystemMessage;
import com.l2jserver.gameserver.taskmanager.DecayTaskManager;
import com.l2jserver.gameserver.util.Util;
import com.l2jserver.util.Rnd;
public class L2Attackable extends L2Npc
{
// Raid
private boolean _isRaid = false;
private boolean _isRaidMinion = false;
//
private boolean _champion = false;
private final Map _aggroList = new ConcurrentHashMap<>();
private boolean _isReturningToSpawnPoint = false;
private boolean _canReturnToSpawnPoint = true;
private boolean _seeThroughSilentMove = false;
// Manor
private boolean _seeded = false;
private L2Seed _seed = null;
private int _seederObjId = 0;
private final AtomicReference _harvestItem = new AtomicReference<>();
// Spoil
private int _spoilerObjectId;
private final AtomicReference> _sweepItems = new AtomicReference<>();
// Over-hit
private boolean _overhit;
private double _overhitDamage;
private L2Character _overhitAttacker;
// Command channel
private volatile L2CommandChannel _firstCommandChannelAttacked = null;
private CommandChannelTimer _commandChannelTimer = null;
private long _commandChannelLastAttack = 0;
// Soul crystal
private boolean _absorbed;
private final Map _absorbersList = new ConcurrentHashMap<>();
// Misc
private boolean _mustGiveExpSp;
protected int _onKillDelay = 5000;
/**
* 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 initialized.
* @param template the template to apply to the NPC.
*/
public L2Attackable(int objectId, L2NpcTemplate template)
{
super(objectId, template);
setInstanceType(InstanceType.L2Attackable);
setIsInvul(false);
_mustGiveExpSp = true;
}
@Override
public AttackableKnownList getKnownList()
{
return (AttackableKnownList) super.getKnownList();
}
@Override
public void initKnownList()
{
setKnownList(new AttackableKnownList(this));
}
@Override
public AttackableStatus getStatus()
{
return (AttackableStatus) super.getStatus();
}
@Override
public void initCharStatus()
{
setStatus(new AttackableStatus(this));
}
@Override
protected L2CharacterAI initAI()
{
return new L2AttackableAI(new AIAccessor());
}
public final Map getAggroList()
{
return _aggroList;
}
public final boolean isReturningToSpawnPoint()
{
return _isReturningToSpawnPoint;
}
public final void setisReturningToSpawnPoint(boolean value)
{
_isReturningToSpawnPoint = value;
}
public final boolean canReturnToSpawnPoint()
{
return _canReturnToSpawnPoint;
}
public final void setCanReturnToSpawnPoint(boolean value)
{
_canReturnToSpawnPoint = value;
}
public boolean canSeeThroughSilentMove()
{
return _seeThroughSilentMove;
}
public void setSeeThroughSilentMove(boolean val)
{
_seeThroughSilentMove = val;
}
/**
* Use the skill if minimum checks are pass.
* @param skill the skill
*/
public void useMagic(Skill skill)
{
if ((skill == null) || isAlikeDead() || skill.isPassive() || isCastingNow() || isSkillDisabled(skill))
{
return;
}
if ((getCurrentMp() < (getStat().getMpConsume(skill) + getStat().getMpInitialConsume(skill))) || (getCurrentHp() <= skill.getHpConsume()))
{
return;
}
if (!skill.isStatic())
{
if (skill.isMagic())
{
if (isMuted())
{
return;
}
}
else
{
if (isPhysicalMuted())
{
return;
}
}
}
final L2Object target = skill.getFirstOfTargetList(this);
if (target != null)
{
getAI().setIntention(CtrlIntention.AI_INTENTION_CAST, skill, target);
}
}
/**
* Reduce the current HP of the L2Attackable.
* @param damage The HP decrease value
* @param attacker The L2Character who attacks
*/
@Override
public void reduceCurrentHp(double damage, L2Character attacker, Skill skill)
{
reduceCurrentHp(damage, attacker, true, false, skill);
}
/**
* Reduce the current HP of the L2Attackable, update its _aggroList and launch the doDie Task if necessary.
* @param damage The HP decrease value
* @param attacker The L2Character who attacks
* @param awake The awake state (If True : stop sleeping)
* @param isDOT
* @param skill
*/
@Override
public void reduceCurrentHp(double damage, L2Character attacker, boolean awake, boolean isDOT, Skill skill)
{
if (isRaid() && !isMinion() && (attacker != null) && (attacker.getParty() != null) && attacker.getParty().isInCommandChannel() && attacker.getParty().getCommandChannel().meetRaidWarCondition(this))
{
if (_firstCommandChannelAttacked == null) // looting right isn't set
{
synchronized (this)
{
if (_firstCommandChannelAttacked == null)
{
_firstCommandChannelAttacked = attacker.getParty().getCommandChannel();
if (_firstCommandChannelAttacked != null)
{
_commandChannelTimer = new CommandChannelTimer(this);
_commandChannelLastAttack = System.currentTimeMillis();
ThreadPoolManager.getInstance().scheduleGeneral(_commandChannelTimer, 10000); // check for last attack
_firstCommandChannelAttacked.broadcastPacket(new CreatureSay(0, Say2.PARTYROOM_ALL, "", "You have looting rights!")); // TODO: retail msg
}
}
}
}
else if (attacker.getParty().getCommandChannel().equals(_firstCommandChannelAttacked)) // is in same channel
{
_commandChannelLastAttack = System.currentTimeMillis(); // update last attack time
}
}
if (isEventMob())
{
return;
}
// Add damage and hate to the attacker AggroInfo of the L2Attackable _aggroList
if (attacker != null)
{
addDamage(attacker, (int) damage, skill);
}
// 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 (master.hasMinions())
{
master.getMinionList().onAssist(this, attacker);
}
master = master.getLeader();
if ((master != null) && master.hasMinions())
{
master.getMinionList().onAssist(this, attacker);
}
}
// Reduce the current HP of the L2Attackable and launch the doDie Task if necessary
super.reduceCurrentHp(damage, attacker, awake, isDOT, skill);
}
public synchronized void setMustRewardExpSp(boolean value)
{
_mustGiveExpSp = value;
}
public synchronized boolean getMustRewardExpSP()
{
return _mustGiveExpSp;
}
/**
* 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
*/
@Override
public boolean doDie(L2Character killer)
{
// Kill the L2NpcInstance (the corpse disappeared after 7 seconds)
if (!super.doDie(killer))
{
return false;
}
if ((killer != null) && killer.isPlayable())
{
// Delayed notification
EventDispatcher.getInstance().notifyEventAsyncDelayed(new OnAttackableKill(killer.getActingPlayer(), this, killer.isSummon()), this, _onKillDelay);
}
// Notify to minions if there are.
if (isMonster())
{
final L2MonsterInstance mob = (L2MonsterInstance) this;
if ((mob.getLeader() != null) && mob.getLeader().hasMinions())
{
final int respawnTime = Config.MINIONS_RESPAWN_TIME.containsKey(getId()) ? Config.MINIONS_RESPAWN_TIME.get(getId()) * 1000 : -1;
mob.getLeader().getMinionList().onMinionDie(mob, respawnTime);
}
if (mob.hasMinions())
{
mob.getMinionList().onMasterDie(false);
}
}
return true;
}
/**
* 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 L2ServitorInstance (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
*/
@Override
protected void calculateRewards(L2Character lastAttacker)
{
try
{
if (getAggroList().isEmpty())
{
return;
}
// NOTE: Concurrent-safe map is used because while iterating to verify all conditions sometimes an entry must be removed.
final Map rewards = new ConcurrentHashMap<>();
L2PcInstance maxDealer = null;
int maxDamage = 0;
long totalDamage = 0;
// While Iterating over This Map Removing Object is Not Allowed
// Go through the _aggroList of the L2Attackable
for (AggroInfo info : getAggroList().values())
{
if (info == null)
{
continue;
}
// Get the L2Character corresponding to this attacker
final L2PcInstance attacker = info.getAttacker().getActingPlayer();
if (attacker != null)
{
// Get damages done by this attacker
final int damage = info.getDamage();
// Prevent unwanted behavior
if (damage > 1)
{
// Check if damage dealer isn't too far from this (killed monster)
if (!Util.checkIfInRange(Config.ALT_PARTY_RANGE, this, attacker, true))
{
continue;
}
totalDamage += damage;
// Calculate real damages (Summoners should get own damage plus summon's damage)
DamageDoneInfo reward = rewards.get(attacker);
if (reward == null)
{
reward = new DamageDoneInfo(attacker, damage);
rewards.put(attacker, reward);
}
else
{
reward.addDamage(damage);
}
if (reward.getDamage() > maxDamage)
{
maxDealer = attacker;
maxDamage = reward.getDamage();
}
}
}
}
// Manage Base, Quests and Sweep drops of the L2Attackable
doItemDrop((maxDealer != null) && maxDealer.isOnline() ? maxDealer : lastAttacker);
// Manage drop of Special Events created by GM for a defined period
doEventDrop(lastAttacker);
if (!getMustRewardExpSP())
{
return;
}
if (!rewards.isEmpty())
{
for (DamageDoneInfo reward : rewards.values())
{
if (reward == null)
{
continue;
}
// Attacker to be rewarded
final L2PcInstance attacker = reward.getAttacker();
// Total amount of damage done
final int damage = reward.getDamage();
// Get party
final L2Party attackerParty = attacker.getParty();
// Penalty applied to the attacker's XP
// If this attacker have servitor, get Exp Penalty applied for the servitor.
final float penalty = attacker.hasServitor() ? ((L2ServitorInstance) attacker.getSummon()).getExpMultiplier() : 1;
// If there's NO party in progress
if (attackerParty == null)
{
// Calculate Exp and SP rewards
if (attacker.getKnownList().knowsObject(this))
{
// Calculate the difference of level between this attacker (player or servitor 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)
final int levelDiff = attacker.getLevel() - getLevel();
final int[] expSp = calculateExpAndSp(levelDiff, damage, totalDamage);
long exp = expSp[0];
int sp = expSp[1];
if (Config.L2JMOD_CHAMPION_ENABLE && isChampion())
{
exp *= Config.L2JMOD_CHAMPION_REWARDS;
sp *= Config.L2JMOD_CHAMPION_REWARDS;
}
exp *= penalty;
// Check for an over-hit enabled strike
L2Character overhitAttacker = getOverhitAttacker();
if (isOverhit() && (overhitAttacker != null) && (overhitAttacker.getActingPlayer() != null) && (attacker == overhitAttacker.getActingPlayer()))
{
attacker.sendPacket(SystemMessageId.OVER_HIT);
exp += calculateOverhitExp(exp);
}
// Distribute the Exp and SP between the L2PcInstance and its L2Summon
if (!attacker.isDead())
{
final long addexp = Math.round(attacker.calcStat(Stats.EXPSP_RATE, exp, null, null));
final int addsp = (int) attacker.calcStat(Stats.EXPSP_RATE, sp, null, null);
attacker.addExpAndSp(addexp, addsp, useVitalityRate());
if (addexp > 0)
{
attacker.updateVitalityPoints(getVitalityPoints(damage), true, false);
}
}
}
}
else
{
// share with party members
int partyDmg = 0;
float partyMul = 1;
int partyLvl = 0;
// Get all L2Character that can be rewarded in the party
final List rewardedMembers = new ArrayList<>();
// Go through all L2PcInstance in the party
final List groupMembers = attackerParty.isInCommandChannel() ? attackerParty.getCommandChannel().getMembers() : attackerParty.getMembers();
for (L2PcInstance partyPlayer : groupMembers)
{
if ((partyPlayer == null) || partyPlayer.isDead())
{
continue;
}
// Get the RewardInfo of this L2PcInstance from L2Attackable rewards
final DamageDoneInfo reward2 = rewards.get(partyPlayer);
// If the L2PcInstance is in the L2Attackable rewards add its damages to party damages
if (reward2 != null)
{
if (Util.checkIfInRange(Config.ALT_PARTY_RANGE, this, partyPlayer, true))
{
partyDmg += reward2.getDamage(); // Add L2PcInstance damages to party damages
rewardedMembers.add(partyPlayer);
if (partyPlayer.getLevel() > partyLvl)
{
if (attackerParty.isInCommandChannel())
{
partyLvl = attackerParty.getCommandChannel().getLevel();
}
else
{
partyLvl = partyPlayer.getLevel();
}
}
}
rewards.remove(partyPlayer); // Remove the L2PcInstance from the L2Attackable rewards
}
else
{
// Add L2PcInstance of the party (that have attacked or not) to members that can be rewarded
// and in range of the monster.
if (Util.checkIfInRange(Config.ALT_PARTY_RANGE, this, partyPlayer, true))
{
rewardedMembers.add(partyPlayer);
if (partyPlayer.getLevel() > partyLvl)
{
if (attackerParty.isInCommandChannel())
{
partyLvl = attackerParty.getCommandChannel().getLevel();
}
else
{
partyLvl = partyPlayer.getLevel();
}
}
}
}
}
// If the party didn't killed this L2Attackable alone
if (partyDmg < totalDamage)
{
partyMul = ((float) partyDmg / totalDamage);
}
// Calculate the level difference between Party and L2Attackable
final int levelDiff = partyLvl - getLevel();
// Calculate Exp and SP rewards
final int[] expSp = calculateExpAndSp(levelDiff, partyDmg, totalDamage);
long exp = expSp[0];
int sp = expSp[1];
if (Config.L2JMOD_CHAMPION_ENABLE && isChampion())
{
exp *= Config.L2JMOD_CHAMPION_REWARDS;
sp *= Config.L2JMOD_CHAMPION_REWARDS;
}
exp *= partyMul;
sp *= partyMul;
// Check for an over-hit enabled strike
// (When in party, the over-hit exp bonus is given to the whole party and splitted proportionally through the party members)
L2Character overhitAttacker = getOverhitAttacker();
if (isOverhit() && (overhitAttacker != null) && (overhitAttacker.getActingPlayer() != null) && (attacker == overhitAttacker.getActingPlayer()))
{
attacker.sendPacket(SystemMessageId.OVER_HIT);
exp += calculateOverhitExp(exp);
}
// Distribute Experience and SP rewards to L2PcInstance Party members in the known area of the last attacker
if (partyDmg > 0)
{
attackerParty.distributeXpAndSp(exp, sp, rewardedMembers, partyLvl, partyDmg, this);
}
}
}
}
}
catch (Exception e)
{
_log.log(Level.SEVERE, "", e);
}
}
@Override
public void addAttackerToAttackByList(L2Character player)
{
if ((player == null) || (player == this) || getAttackByList().contains(player))
{
return;
}
getAttackByList().add(player);
}
/**
* 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 skill
*/
public void addDamage(L2Character attacker, int damage, Skill skill)
{
if (attacker == null)
{
return;
}
// Notify the L2Attackable AI with EVT_ATTACKED
if (!isDead())
{
try
{
// If monster is on walk - stop it
if (isWalker() && !isCoreAIDisabled() && WalkingManager.getInstance().isOnWalk(this))
{
WalkingManager.getInstance().stopMoving(this, false, true);
}
getAI().notifyEvent(CtrlEvent.EVT_ATTACKED, attacker);
addDamageHate(attacker, damage, (damage * 100) / (getLevel() + 7));
final L2PcInstance player = attacker.getActingPlayer();
if (player != null)
{
EventDispatcher.getInstance().notifyEventAsync(new OnAttackableAttack(player, this, damage, skill, attacker.isSummon()), this);
}
}
catch (Exception e)
{
_log.log(Level.SEVERE, "", e);
}
}
}
/**
* 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;
}
final L2PcInstance targetPlayer = attacker.getActingPlayer();
// Get the AggroInfo of the attacker L2Character from the _aggroList of the L2Attackable
AggroInfo ai = getAggroList().get(attacker);
if (ai == null)
{
ai = new AggroInfo(attacker);
getAggroList().put(attacker, ai);
}
ai.addDamage(damage);
// traps does not cause aggro
// making this hack because not possible to determine if damage made by trap
// so just check for triggered trap here
if ((targetPlayer == null) || (targetPlayer.getTrap() == null) || !targetPlayer.getTrap().isTriggered())
{
ai.addHate(aggro);
}
if ((targetPlayer != null) && (aggro == 0))
{
addDamageHate(attacker, 0, 1);
// Set the intention to the L2Attackable to AI_INTENTION_ACTIVE
if (getAI().getIntention() == CtrlIntention.AI_INTENTION_IDLE)
{
getAI().setIntention(CtrlIntention.AI_INTENTION_ACTIVE);
}
// Notify to scripts
EventDispatcher.getInstance().notifyEventAsync(new OnAttackableAggroRangeEnter(this, targetPlayer, attacker.isSummon()), this);
}
else if ((targetPlayer == null) && (aggro == 0))
{
aggro = 1;
ai.addHate(1);
}
// 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);
}
}
public void reduceHate(L2Character target, int amount)
{
if ((getAI() instanceof L2SiegeGuardAI) || (getAI() instanceof L2FortSiegeGuardAI))
{
// TODO: this just prevents error until siege guards are handled properly
stopHating(target);
setTarget(null);
getAI().setIntention(CtrlIntention.AI_INTENTION_IDLE);
return;
}
if (target == null) // whole aggrolist
{
L2Character mostHated = getMostHated();
if (mostHated == null) // makes target passive for a moment more
{
((L2AttackableAI) getAI()).setGlobalAggro(-25);
return;
}
for (AggroInfo ai : getAggroList().values())
{
if (ai == null)
{
return;
}
ai.addHate(amount);
}
amount = getHating(mostHated);
if (amount >= 0)
{
((L2AttackableAI) getAI()).setGlobalAggro(-25);
clearAggroList();
getAI().setIntention(CtrlIntention.AI_INTENTION_ACTIVE);
setWalking();
}
return;
}
AggroInfo ai = getAggroList().get(target);
if (ai == null)
{
_log.info("target " + target + " not present in aggro list of " + this);
return;
}
ai.addHate(amount);
if ((ai.getHate() >= 0) && (getMostHated() == null))
{
((L2AttackableAI) getAI()).setGlobalAggro(-25);
clearAggroList();
getAI().setIntention(CtrlIntention.AI_INTENTION_ACTIVE);
setWalking();
}
}
/**
* Clears _aggroList hate of the L2Character without removing from the list.
* @param target
*/
public void stopHating(L2Character target)
{
if (target == null)
{
return;
}
AggroInfo ai = getAggroList().get(target);
if (ai != null)
{
ai.stopHate();
}
}
/**
* @return the most hated L2Character of the L2Attackable _aggroList.
*/
public L2Character getMostHated()
{
if (getAggroList().isEmpty() || isAlikeDead())
{
return null;
}
L2Character mostHated = null;
int maxHate = 0;
// While Interacting over This Map Removing Object is Not Allowed
// Go through the aggroList of the L2Attackable
for (AggroInfo ai : getAggroList().values())
{
if (ai == null)
{
continue;
}
if (ai.checkHate(this) > maxHate)
{
mostHated = ai.getAttacker();
maxHate = ai.getHate();
}
}
return mostHated;
}
/**
* @return the 2 most hated L2Character of the L2Attackable _aggroList.
*/
public List get2MostHated()
{
if (getAggroList().isEmpty() || isAlikeDead())
{
return null;
}
L2Character mostHated = null;
L2Character secondMostHated = null;
int maxHate = 0;
List result = new ArrayList<>();
// While iterating over this map removing objects is not allowed
// Go through the aggroList of the L2Attackable
for (AggroInfo ai : getAggroList().values())
{
if (ai == null)
{
continue;
}
if (ai.checkHate(this) > maxHate)
{
secondMostHated = mostHated;
mostHated = ai.getAttacker();
maxHate = ai.getHate();
}
}
result.add(mostHated);
if (getAttackByList().contains(secondMostHated))
{
result.add(secondMostHated);
}
else
{
result.add(null);
}
return result;
}
public List getHateList()
{
if (getAggroList().isEmpty() || isAlikeDead())
{
return null;
}
List result = new ArrayList<>();
for (AggroInfo ai : getAggroList().values())
{
if (ai == null)
{
continue;
}
ai.checkHate(this);
result.add(ai.getAttacker());
}
return result;
}
/**
* @param target The L2Character whose hate level must be returned
* @return the hate level of the L2Attackable against this L2Character contained in _aggroList.
*/
public int getHating(final L2Character target)
{
if (getAggroList().isEmpty() || (target == null))
{
return 0;
}
final AggroInfo ai = getAggroList().get(target);
if (ai == null)
{
return 0;
}
if (ai.getAttacker() instanceof L2PcInstance)
{
L2PcInstance act = (L2PcInstance) ai.getAttacker();
if (act.isInvisible() || ai.getAttacker().isInvul() || act.isSpawnProtected())
{
// Remove Object Should Use This Method and Can be Blocked While Interacting
getAggroList().remove(target);
return 0;
}
}
if (!ai.getAttacker().isVisible() || ai.getAttacker().isInvisible())
{
getAggroList().remove(target);
return 0;
}
if (ai.getAttacker().isAlikeDead())
{
ai.stopHate();
return 0;
}
return ai.getHate();
}
public void doItemDrop(L2Character mainDamageDealer)
{
doItemDrop(getTemplate(), mainDamageDealer);
}
/**
* 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 automatically.
* 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 the 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 npcTemplate
* @param mainDamageDealer
*/
public void doItemDrop(L2NpcTemplate npcTemplate, L2Character mainDamageDealer)
{
if (mainDamageDealer == null)
{
return;
}
L2PcInstance player = mainDamageDealer.getActingPlayer();
// Don't drop anything if the last attacker or owner isn't L2PcInstance
if (player == null)
{
return;
}
CursedWeaponsManager.getInstance().checkDrop(this, player);
if (isSpoiled())
{
_sweepItems.set(npcTemplate.calculateDrops(DropListScope.CORPSE, this, player));
}
Collection deathItems = npcTemplate.calculateDrops(DropListScope.DEATH, this, player);
if (deathItems != null)
{
for (ItemHolder drop : deathItems)
{
L2Item item = ItemTable.getInstance().getTemplate(drop.getId());
// Check if the autoLoot mode is active
if (isFlying() || (!item.hasExImmediateEffect() && ((!isRaid() && Config.AUTO_LOOT) || (isRaid() && Config.AUTO_LOOT_RAIDS))) || (item.hasExImmediateEffect() && Config.AUTO_LOOT_HERBS))
{
player.doAutoLoot(this, drop); // Give the item(s) to the L2PcInstance that has killed the L2Attackable
}
else
{
dropItem(player, drop); // drop the item on the ground
}
// Broadcast message if RaidBoss was defeated
if (isRaid() && !isRaidMinion())
{
final SystemMessage sm = SystemMessage.getSystemMessage(SystemMessageId.C1_DIED_DROPPED_S3_S2);
sm.addCharName(this);
sm.addItemName(item);
sm.addLong(drop.getCount());
broadcastPacket(sm);
}
}
}
// Apply Special Item drop with random(rnd) quantity(qty) for champions.
if (Config.L2JMOD_CHAMPION_ENABLE && isChampion() && ((Config.L2JMOD_CHAMPION_REWARD_LOWER_LVL_ITEM_CHANCE > 0) || (Config.L2JMOD_CHAMPION_REWARD_HIGHER_LVL_ITEM_CHANCE > 0)))
{
int champqty = Rnd.get(Config.L2JMOD_CHAMPION_REWARD_QTY);
ItemHolder item = new ItemHolder(Config.L2JMOD_CHAMPION_REWARD_ID, ++champqty);
if ((player.getLevel() <= getLevel()) && (Rnd.get(100) < Config.L2JMOD_CHAMPION_REWARD_LOWER_LVL_ITEM_CHANCE))
{
if (Config.AUTO_LOOT || isFlying())
{
player.addItem("ChampionLoot", item.getId(), item.getCount(), this, true); // Give the item(s) to the L2PcInstance that has killed the L2Attackable
}
else
{
dropItem(player, item);
}
}
else if ((player.getLevel() > getLevel()) && (Rnd.get(100) < Config.L2JMOD_CHAMPION_REWARD_HIGHER_LVL_ITEM_CHANCE))
{
if (Config.AUTO_LOOT || isFlying())
{
player.addItem("ChampionLoot", item.getId(), item.getCount(), this, true); // Give the item(s) to the L2PcInstance that has killed the L2Attackable
}
else
{
dropItem(player, item);
}
}
}
}
/**
* 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 automatically.
* 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 the 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)
{
if (lastAttacker == null)
{
return;
}
L2PcInstance player = lastAttacker.getActingPlayer();
// Don't drop anything if the last attacker or owner isn't L2PcInstance
if (player == null)
{
return;
}
if ((player.getLevel() - getLevel()) > 9)
{
return;
}
// Go through DateDrop of EventDroplist allNpcDateDrops within the date range
for (DateDrop drop : EventDroplist.getInstance().getAllDrops())
{
if (Rnd.get(1000000) < drop.getEventDrop().getDropChance())
{
final int itemId = drop.getEventDrop().getItemIdList()[Rnd.get(drop.getEventDrop().getItemIdList().length)];
final long itemCount = Rnd.get(drop.getEventDrop().getMinCount(), drop.getEventDrop().getMaxCount());
if (Config.AUTO_LOOT || isFlying())
{
player.doAutoLoot(this, itemId, itemCount); // Give the item(s) to the L2PcInstance that has killed the L2Attackable
}
else
{
dropItem(player, itemId, itemCount); // drop the item on the ground
}
}
}
}
/**
* @return the active weapon of this L2Attackable (= null).
*/
public L2ItemInstance getActiveWeapon()
{
return null;
}
/**
* @param player The L2Character searched in the _aggroList of the L2Attackable
* @return True if the _aggroList of this L2Attackable contains the L2Character.
*/
public boolean containsTarget(L2Character player)
{
return getAggroList().containsKey(player);
}
/**
* Clear the _aggroList of the L2Attackable.
*/
public void clearAggroList()
{
getAggroList().clear();
// clear overhit values
_overhit = false;
_overhitDamage = 0;
_overhitAttacker = null;
}
/**
* @return {@code true} if there is a loot to sweep, {@code false} otherwise.
*/
@Override
public boolean isSweepActive()
{
return _sweepItems.get() != null;
}
/**
* @return a copy of dummy items for the spoil loot.
*/
public List getSpoilLootItems()
{
final Collection sweepItems = _sweepItems.get();
final List lootItems = new LinkedList<>();
if (sweepItems != null)
{
for (ItemHolder item : sweepItems)
{
lootItems.add(ItemTable.getInstance().getTemplate(item.getId()));
}
}
return lootItems;
}
/**
* @return table containing all L2ItemInstance that can be spoiled.
*/
public Collection takeSweep()
{
return _sweepItems.getAndSet(null);
}
/**
* @return table containing all L2ItemInstance that can be harvested.
*/
public ItemHolder takeHarvest()
{
return _harvestItem.getAndSet(null);
}
/**
* Checks if the corpse is too old.
* @param attacker the player to validate
* @param remainingTime the time to check
* @param sendMessage if {@code true} will send a message of corpse too old
* @return {@code true} if the corpse is too old
*/
public boolean isOldCorpse(L2PcInstance attacker, int remainingTime, boolean sendMessage)
{
if (isDead() && (DecayTaskManager.getInstance().getRemainingTime(this) < remainingTime))
{
if (sendMessage && (attacker != null))
{
attacker.sendPacket(SystemMessageId.CORPSE_TOO_OLD_SKILL_NOT_USED);
}
return true;
}
return false;
}
/**
* @param sweeper the player to validate.
* @param sendMessage sendMessage if {@code true} will send a message of sweep not allowed.
* @return {@code true} if is the spoiler or is in the spoiler party.
*/
public boolean checkSpoilOwner(L2PcInstance sweeper, boolean sendMessage)
{
if ((sweeper.getObjectId() != getSpoilerObjectId()) && !sweeper.isInLooterParty(getSpoilerObjectId()))
{
if (sendMessage)
{
sweeper.sendPacket(SystemMessageId.SWEEP_NOT_ALLOWED);
}
return false;
}
return true;
}
/**
* Set the over-hit flag on the L2Attackable.
* @param status The status of the over-hit flag
*/
public void overhitEnabled(boolean status)
{
_overhit = status;
}
/**
* Set the over-hit values like the attacker who did the strike and the amount of damage done by the skill.
* @param attacker The L2Character who hit on the L2Attackable using the over-hit enabled skill
* @param damage The amount of damage done by the over-hit enabled skill on the L2Attackable
*/
public void setOverhitValues(L2Character attacker, double damage)
{
// Calculate the over-hit damage
// Ex: mob had 10 HP left, over-hit skill did 50 damage total, over-hit damage is 40
double overhitDmg = -(getCurrentHp() - damage);
if (overhitDmg < 0)
{
// we didn't killed the mob with the over-hit strike. (it wasn't really an over-hit strike)
// let's just clear all the over-hit related values
overhitEnabled(false);
_overhitDamage = 0;
_overhitAttacker = null;
return;
}
overhitEnabled(true);
_overhitDamage = overhitDmg;
_overhitAttacker = attacker;
}
/**
* Return the L2Character who hit on the L2Attackable using an over-hit enabled skill.
* @return L2Character attacker
*/
public L2Character getOverhitAttacker()
{
return _overhitAttacker;
}
/**
* Return the amount of damage done on the L2Attackable using an over-hit enabled skill.
* @return double damage
*/
public double getOverhitDamage()
{
return _overhitDamage;
}
/**
* @return True if the L2Attackable was hit by an over-hit enabled skill.
*/
public boolean isOverhit()
{
return _overhit;
}
/**
* 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.
* @param attacker
*/
public void addAbsorber(L2PcInstance attacker)
{
// If we have no _absorbersList initiated, do it
final AbsorberInfo ai = _absorbersList.get(attacker.getObjectId());
// If the L2Character attacker isn't already in the _absorbersList of this L2Attackable, add it
if (ai == null)
{
_absorbersList.put(attacker.getObjectId(), new AbsorberInfo(attacker.getObjectId(), getCurrentHp()));
}
else
{
ai.setAbsorbedHp(getCurrentHp());
}
// Set this L2Attackable as absorbed
absorbSoul();
}
public void resetAbsorbList()
{
_absorbed = false;
_absorbersList.clear();
}
public Map getAbsorbersList()
{
return _absorbersList;
}
/**
* Calculate the Experience and SP to distribute to attacker (L2PcInstance, L2ServitorInstance or L2Party) of the L2Attackable.
* @param diff The difference of level between attacker (L2PcInstance, L2ServitorInstance or L2Party) and the L2Attackable
* @param damage The damages given by the attacker (L2PcInstance, L2ServitorInstance or L2Party)
* @param totalDamage The total damage done
* @return
*/
private int[] calculateExpAndSp(int diff, int damage, long totalDamage)
{
double xp;
double sp;
if (diff < -5)
{
diff = -5; // makes possible to use ALT_GAME_EXPONENT configuration
}
xp = ((double) getExpReward() * damage) / totalDamage;
if (Config.ALT_GAME_EXPONENT_XP != 0)
{
xp *= Math.pow(2., -diff / Config.ALT_GAME_EXPONENT_XP);
}
sp = ((double) getSpReward() * damage) / totalDamage;
if (Config.ALT_GAME_EXPONENT_SP != 0)
{
sp *= Math.pow(2., -diff / Config.ALT_GAME_EXPONENT_SP);
}
if ((Config.ALT_GAME_EXPONENT_XP == 0) && (Config.ALT_GAME_EXPONENT_SP == 0))
{
if (diff > 5) // formula revised May 07
{
double pow = Math.pow((double) 5 / 6, diff - 5);
xp = xp * pow;
sp = sp * pow;
}
if (xp <= 0)
{
xp = 0;
sp = 0;
}
else if (sp <= 0)
{
sp = 0;
}
}
int[] tmp =
{
(int) xp,
(int) sp
};
return tmp;
}
public long calculateOverhitExp(long normalExp)
{
// Get the percentage based on the total of extra (over-hit) damage done relative to the total (maximum) ammount of HP on the L2Attackable
double overhitPercentage = ((getOverhitDamage() * 100) / getMaxHp());
// Over-hit damage percentages are limited to 25% max
if (overhitPercentage > 25)
{
overhitPercentage = 25;
}
// Get the overhit exp bonus according to the above over-hit damage percentage
// (1/1 basis - 13% of over-hit damage, 13% of extra exp is given, and so on...)
double overhitExp = ((overhitPercentage / 100) * normalExp);
// Return the rounded ammount of exp points to be added to the player's normal exp reward
long bonusOverhit = Math.round(overhitExp);
return bonusOverhit;
}
/**
* Return True.
*/
@Override
public boolean canBeAttacked()
{
return true;
}
@Override
public void onSpawn()
{
super.onSpawn();
// Clear mob spoil, seed
setSpoilerObjectId(0);
// Clear all aggro char from list
clearAggroList();
// Clear Harvester reward
_harvestItem.set(null);
// Clear mod Seeded stat
_seeded = false;
_seed = null;
_seederObjId = 0;
// Clear overhit value
overhitEnabled(false);
_sweepItems.set(null);
resetAbsorbList();
setWalking();
// check the region where this mob is, do not activate the AI if region is inactive.
if (!isInActiveRegion())
{
if (hasAI())
{
getAI().stopAITask();
}
}
}
/**
* Checks if its spoiled.
* @return {@code true} if its spoiled, {@code false} otherwise
*/
public final boolean isSpoiled()
{
return _spoilerObjectId != 0;
}
/**
* Gets the spoiler object id.
* @return the spoiler object id if its spoiled, 0 otherwise
*/
public final int getSpoilerObjectId()
{
return _spoilerObjectId;
}
/**
* Sets the spoiler object id.
* @param spoilerObjectId the spoiler object id
*/
public final void setSpoilerObjectId(int spoilerObjectId)
{
_spoilerObjectId = spoilerObjectId;
}
/**
* Sets state of the mob to seeded. Parameters needed to be set before.
* @param seeder
*/
public final void setSeeded(L2PcInstance seeder)
{
if ((_seed != null) && (_seederObjId == seeder.getObjectId()))
{
_seeded = true;
int count = 1;
for (int skillId : getTemplate().getSkills().keySet())
{
switch (skillId)
{
case 4303: // Strong type x2
count *= 2;
break;
case 4304: // Strong type x3
count *= 3;
break;
case 4305: // Strong type x4
count *= 4;
break;
case 4306: // Strong type x5
count *= 5;
break;
case 4307: // Strong type x6
count *= 6;
break;
case 4308: // Strong type x7
count *= 7;
break;
case 4309: // Strong type x8
count *= 8;
break;
case 4310: // Strong type x9
count *= 9;
break;
}
}
// hi-lvl mobs bonus
final int diff = getLevel() - _seed.getLevel() - 5;
if (diff > 0)
{
count += diff;
}
_harvestItem.set(new ItemHolder(_seed.getCropId(), count * Config.RATE_DROP_MANOR));
}
}
/**
* Sets the seed parameters, but not the seed state
* @param seed - instance {@link L2Seed} of used seed
* @param seeder - player who sows the seed
*/
public final void setSeeded(L2Seed seed, L2PcInstance seeder)
{
if (!_seeded)
{
_seed = seed;
_seederObjId = seeder.getObjectId();
}
}
public final int getSeederId()
{
return _seederObjId;
}
public final L2Seed getSeed()
{
return _seed;
}
public final boolean isSeeded()
{
return _seeded;
}
/**
* Set delay for onKill() call, in ms Default: 5000 ms
* @param delay
*/
public final void setOnKillDelay(int delay)
{
_onKillDelay = delay;
}
public final int getOnKillDelay()
{
return _onKillDelay;
}
/**
* Check if the server allows Random Animation.
*/
// This is located here because L2Monster and L2FriendlyMob both extend this class. The other non-pc instances extend either L2NpcInstance or L2MonsterInstance.
@Override
public boolean hasRandomAnimation()
{
return ((Config.MAX_MONSTER_ANIMATION > 0) && isRandomAnimationEnabled() && !(this instanceof L2GrandBossInstance));
}
@Override
public boolean isMob()
{
return true; // This means we use MAX_MONSTER_ANIMATION instead of MAX_NPC_ANIMATION
}
public void setCommandChannelTimer(CommandChannelTimer commandChannelTimer)
{
_commandChannelTimer = commandChannelTimer;
}
public CommandChannelTimer getCommandChannelTimer()
{
return _commandChannelTimer;
}
public L2CommandChannel getFirstCommandChannelAttacked()
{
return _firstCommandChannelAttacked;
}
public void setFirstCommandChannelAttacked(L2CommandChannel firstCommandChannelAttacked)
{
_firstCommandChannelAttacked = firstCommandChannelAttacked;
}
/**
* @return the _commandChannelLastAttack
*/
public long getCommandChannelLastAttack()
{
return _commandChannelLastAttack;
}
/**
* @param channelLastAttack the _commandChannelLastAttack to set
*/
public void setCommandChannelLastAttack(long channelLastAttack)
{
_commandChannelLastAttack = channelLastAttack;
}
public void returnHome()
{
clearAggroList();
if (hasAI() && (getSpawn() != null))
{
getAI().setIntention(CtrlIntention.AI_INTENTION_MOVE_TO, getSpawn().getLocation(this));
}
}
/*
* Return vitality points decrease (if positive) or increase (if negative) based on damage. Maximum for damage = maxHp.
*/
public float getVitalityPoints(int damage)
{
// sanity check
if (damage <= 0)
{
return 0;
}
final float divider = (getLevel() > 0) && (getExpReward() > 0) ? (getTemplate().getBaseHpMax() * 9 * getLevel() * getLevel()) / (100 * getExpReward()) : 0;
if (divider == 0)
{
return 0;
}
// negative value - vitality will be consumed
return -Math.min(damage, getMaxHp()) / divider;
}
/*
* True if vitality rate for exp and sp should be applied
*/
public boolean useVitalityRate()
{
if (isChampion() && !Config.L2JMOD_CHAMPION_ENABLE_VITALITY)
{
return false;
}
return true;
}
/** Return True if the L2Character is RaidBoss or his minion. */
@Override
public boolean isRaid()
{
return _isRaid;
}
/**
* Set this Npc as a Raid instance.
* @param isRaid
*/
public void setIsRaid(boolean isRaid)
{
_isRaid = isRaid;
}
/**
* Set this Npc as a Minion instance.
* @param val
*/
public void setIsRaidMinion(boolean val)
{
_isRaid = val;
_isRaidMinion = val;
}
@Override
public boolean isRaidMinion()
{
return _isRaidMinion;
}
@Override
public boolean isMinion()
{
return getLeader() != null;
}
/**
* @return leader of this minion or null.
*/
public L2Attackable getLeader()
{
return null;
}
public void setChampion(boolean champ)
{
_champion = champ;
}
@Override
public boolean isChampion()
{
return _champion;
}
@Override
public boolean isAttackable()
{
return true;
}
}