/*
* 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 <http://www.gnu.org/licenses/>.
*/
package com.l2jserver.gameserver.model.actor.instance;
import static com.l2jserver.gameserver.ai.CtrlIntention.AI_INTENTION_IDLE;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.Future;
import com.l2jserver.gameserver.ThreadPoolManager;
import com.l2jserver.gameserver.ai.CtrlIntention;
import com.l2jserver.gameserver.data.xml.impl.NpcData;
import com.l2jserver.gameserver.datatables.SkillData;
import com.l2jserver.gameserver.enums.InstanceType;
import com.l2jserver.gameserver.model.L2Object;
import com.l2jserver.gameserver.model.Location;
import com.l2jserver.gameserver.model.actor.L2Character;
import com.l2jserver.gameserver.model.effects.L2EffectType;
import com.l2jserver.gameserver.model.items.instance.L2ItemInstance;
import com.l2jserver.gameserver.model.skills.Skill;
import com.l2jserver.gameserver.network.serverpackets.ActionFailed;
import com.l2jserver.gameserver.network.serverpackets.NpcInfo;
import com.l2jserver.gameserver.network.serverpackets.SocialAction;
import com.l2jserver.gameserver.network.serverpackets.StopMove;
import com.l2jserver.util.Rnd;
// While a tamed beast behaves a lot like a pet (ingame) and does have
// an owner, in all other aspects, it acts like a mob.
// In addition, it can be fed in order to increase its duration.
// This class handles the running tasks, AI, and feed of the mob.
// The (mostly optional) AI on feeding the spawn is handled by the datapack ai script
public final class L2TamedBeastInstance extends L2FeedableBeastInstance
{
private int _foodSkillId;
private static final int MAX_DISTANCE_FROM_HOME = 30000;
private static final int MAX_DISTANCE_FROM_OWNER = 2000;
private static final int MAX_DURATION = 1200000; // 20 minutes
private static final int DURATION_CHECK_INTERVAL = 60000; // 1 minute
private static final int DURATION_INCREASE_INTERVAL = 20000; // 20 secs (gained upon feeding)
private static final int BUFF_INTERVAL = 5000; // 5 seconds
private int _remainingTime = MAX_DURATION;
private int _homeX, _homeY, _homeZ;
protected L2PcInstance _owner;
private Future<?> _buffTask = null;
private Future<?> _durationCheckTask = null;
protected boolean _isFreyaBeast;
private List<Skill> _beastSkills = null;
public L2TamedBeastInstance(int npcTemplateId)
{
super(NpcData.getInstance().getTemplate(npcTemplateId));
setInstanceType(InstanceType.L2TamedBeastInstance);
setHome(this);
}
public L2TamedBeastInstance(int npcTemplateId, L2PcInstance owner, int foodSkillId, int x, int y, int z)
{
super(NpcData.getInstance().getTemplate(npcTemplateId));
_isFreyaBeast = false;
setInstanceType(InstanceType.L2TamedBeastInstance);
setCurrentHp(getMaxHp());
setCurrentMp(getMaxMp());
setOwner(owner);
setFoodType(foodSkillId);
setHome(x, y, z);
spawnMe(x, y, z);
}
public L2TamedBeastInstance(int npcTemplateId, L2PcInstance owner, int food, int x, int y, int z, boolean isFreyaBeast)
{
super(NpcData.getInstance().getTemplate(npcTemplateId));
_isFreyaBeast = isFreyaBeast;
setInstanceType(InstanceType.L2TamedBeastInstance);
setCurrentHp(getMaxHp());
setCurrentMp(getMaxMp());
setFoodType(food);
setHome(x, y, z);
spawnMe(x, y, z);
setOwner(owner);
if (isFreyaBeast)
{
getAI().setIntention(CtrlIntention.AI_INTENTION_FOLLOW, _owner);
}
}
public void onReceiveFood()
{
// Eating food extends the duration by 20secs, to a max of 20minutes
_remainingTime = _remainingTime + DURATION_INCREASE_INTERVAL;
if (_remainingTime > MAX_DURATION)
{
_remainingTime = MAX_DURATION;
}
}
public Location getHome()
{
return new Location(_homeX, _homeY, _homeZ);
}
public void setHome(int x, int y, int z)
{
_homeX = x;
_homeY = y;
_homeZ = z;
}
public void setHome(L2Character c)
{
setHome(c.getX(), c.getY(), c.getZ());
}
public int getRemainingTime()
{
return _remainingTime;
}
public void setRemainingTime(int duration)
{
_remainingTime = duration;
}
public int getFoodType()
{
return _foodSkillId;
}
public void setFoodType(int foodItemId)
{
if (foodItemId > 0)
{
_foodSkillId = foodItemId;
// start the duration checks
// start the buff tasks
if (_durationCheckTask != null)
{
_durationCheckTask.cancel(true);
}
_durationCheckTask = ThreadPoolManager.getInstance().scheduleGeneralAtFixedRate(new CheckDuration(this), DURATION_CHECK_INTERVAL, DURATION_CHECK_INTERVAL);
}
}
@Override
public boolean doDie(L2Character killer)
{
if (!super.doDie(killer))
{
return false;
}
getAI().stopFollow();
if (_buffTask != null)
{
_buffTask.cancel(true);
}
if (_durationCheckTask != null)
{
_durationCheckTask.cancel(true);
}
// clean up variables
if ((_owner != null) && (_owner.getTrainedBeasts() != null))
{
_owner.getTrainedBeasts().remove(this);
}
_buffTask = null;
_durationCheckTask = null;
_owner = null;
_foodSkillId = 0;
_remainingTime = 0;
return true;
}
@Override
public boolean isAutoAttackable(L2Character attacker)
{
return !_isFreyaBeast;
}
public boolean isFreyaBeast()
{
return _isFreyaBeast;
}
public void addBeastSkill(Skill skill)
{
if (_beastSkills == null)
{
_beastSkills = new ArrayList<>();
}
_beastSkills.add(skill);
}
public void castBeastSkills()
{
if ((_owner == null) || (_beastSkills == null))
{
return;
}
int delay = 100;
for (Skill skill : _beastSkills)
{
ThreadPoolManager.getInstance().scheduleGeneral(new buffCast(skill), delay);
delay += (100 + skill.getHitTime());
}
ThreadPoolManager.getInstance().scheduleGeneral(new buffCast(null), delay);
}
private class buffCast implements Runnable
{
private final Skill _skill;
public buffCast(Skill skill)
{
_skill = skill;
}
@Override
public void run()
{
if (_skill == null)
{
getAI().setIntention(CtrlIntention.AI_INTENTION_FOLLOW, _owner);
}
else
{
sitCastAndFollow(_skill, _owner);
}
}
}
public L2PcInstance getOwner()
{
return _owner;
}
public void setOwner(L2PcInstance owner)
{
if (owner != null)
{
_owner = owner;
setTitle(owner.getName());
// broadcast the new title
setShowSummonAnimation(true);
broadcastPacket(new NpcInfo(this));
owner.addTrainedBeast(this);
// always and automatically follow the owner.
getAI().startFollow(_owner, 100);
if (!_isFreyaBeast)
{
// instead of calculating this value each time, let's get this now and pass it on
int totalBuffsAvailable = 0;
for (Skill skill : getTemplate().getSkills().values())
{
// if the skill is a buff, check if the owner has it already [ owner.getEffect(L2Skill skill) ]
if (skill.isContinuous() && !skill.isDebuff())
{
totalBuffsAvailable++;
}
}
// start the buff tasks
if (_buffTask != null)
{
_buffTask.cancel(true);
}
_buffTask = ThreadPoolManager.getInstance().scheduleGeneralAtFixedRate(new CheckOwnerBuffs(this, totalBuffsAvailable), BUFF_INTERVAL, BUFF_INTERVAL);
}
}
else
{
deleteMe(); // despawn if no owner
}
}
public boolean isTooFarFromHome()
{
return !isInsideRadius(_homeX, _homeY, _homeZ, MAX_DISTANCE_FROM_HOME, true, true);
}
@Override
public boolean deleteMe()
{
if (_buffTask != null)
{
_buffTask.cancel(true);
}
_durationCheckTask.cancel(true);
stopHpMpRegeneration();
// clean up variables
if ((_owner != null) && (_owner.getTrainedBeasts() != null))
{
_owner.getTrainedBeasts().remove(this);
}
setTarget(null);
_buffTask = null;
_durationCheckTask = null;
_owner = null;
_foodSkillId = 0;
_remainingTime = 0;
// remove the spawn
return super.deleteMe();
}
// notification triggered by the owner when the owner is attacked.
// tamed mobs will heal/recharge or debuff the enemy according to their skills
public void onOwnerGotAttacked(L2Character attacker)
{
// check if the owner is no longer around...if so, despawn
if ((_owner == null) || !_owner.isOnline())
{
deleteMe();
return;
}
// if the owner is too far away, stop anything else and immediately run towards the owner.
if (!_owner.isInsideRadius(this, MAX_DISTANCE_FROM_OWNER, true, true))
{
getAI().startFollow(_owner);
return;
}
// if the owner is dead, do nothing...
if (_owner.isDead() || _isFreyaBeast)
{
return;
}
// if the tamed beast is currently in the middle of casting, let it complete its skill...
if (isCastingNow())
{
return;
}
float HPRatio = ((float) _owner.getCurrentHp()) / _owner.getMaxHp();
// if the owner has a lot of HP, then debuff the enemy with a random debuff among the available skills
// use of more than one debuff at this moment is acceptable
if (HPRatio >= 0.8)
{
for (Skill skill : getTemplate().getSkills().values())
{
// if the skill is a debuff, check if the attacker has it already [ attacker.getEffect(L2Skill skill) ]
if (skill.isDebuff() && (Rnd.get(3) < 1) && ((attacker != null) && attacker.isAffectedBySkill(skill.getId())))
{
sitCastAndFollow(skill, attacker);
}
}
}
// for HP levels between 80% and 50%, do not react to attack events (so that MP can regenerate a bit)
// for lower HP ranges, heal or recharge the owner with 1 skill use per attack.
else if (HPRatio < 0.5)
{
int chance = 1;
if (HPRatio < 0.25)
{
chance = 2;
}
// if the owner has a lot of HP, then debuff the enemy with a random debuff among the available skills
for (Skill skill : getTemplate().getSkills().values())
{
// if the skill is a buff, check if the owner has it already [ owner.getEffect(L2Skill skill) ]
if ((Rnd.get(5) < chance) && skill.hasEffectType(L2EffectType.CPHEAL, L2EffectType.HEAL, L2EffectType.MANAHEAL_BY_LEVEL, L2EffectType.MANAHEAL_PERCENT))
{
sitCastAndFollow(skill, _owner);
}
}
}
}
/**
* Prepare and cast a skill:<br>
* First smoothly prepare the beast for casting, by abandoning other actions.<br>
* Next, call super.doCast(skill) in order to actually cast the spell.<br>
* Finally, return to auto-following the owner.
* @param skill
* @param target
*/
protected void sitCastAndFollow(Skill skill, L2Character target)
{
stopMove(null);
broadcastPacket(new StopMove(this));
getAI().setIntention(AI_INTENTION_IDLE);
setTarget(target);
doCast(skill);
getAI().setIntention(CtrlIntention.AI_INTENTION_FOLLOW, _owner);
}
private static class CheckDuration implements Runnable
{
private final L2TamedBeastInstance _tamedBeast;
CheckDuration(L2TamedBeastInstance tamedBeast)
{
_tamedBeast = tamedBeast;
}
@Override
public void run()
{
int foodTypeSkillId = _tamedBeast.getFoodType();
L2PcInstance owner = _tamedBeast.getOwner();
L2ItemInstance item = null;
if (_tamedBeast._isFreyaBeast)
{
item = owner.getInventory().getItemByItemId(foodTypeSkillId);
if ((item != null) && (item.getCount() >= 1))
{
owner.destroyItem("BeastMob", item, 1, _tamedBeast, true);
_tamedBeast.broadcastPacket(new SocialAction(_tamedBeast.getObjectId(), 3));
}
else
{
_tamedBeast.deleteMe();
}
}
else
{
_tamedBeast.setRemainingTime(_tamedBeast.getRemainingTime() - DURATION_CHECK_INTERVAL);
// I tried to avoid this as much as possible...but it seems I can't avoid hardcoding
// ids further, except by carrying an additional variable just for these two lines...
// Find which food item needs to be consumed.
if (foodTypeSkillId == 2188)
{
item = owner.getInventory().getItemByItemId(6643);
}
else if (foodTypeSkillId == 2189)
{
item = owner.getInventory().getItemByItemId(6644);
}
// if the owner has enough food, call the item handler (use the food and triffer all necessary actions)
if ((item != null) && (item.getCount() >= 1))
{
L2Object oldTarget = owner.getTarget();
owner.setTarget(_tamedBeast);
L2Object[] targets =
{
_tamedBeast
};
// emulate a call to the owner using food, but bypass all checks for range, etc
// this also causes a call to the AI tasks handling feeding, which may call onReceiveFood as required.
owner.callSkill(SkillData.getInstance().getSkill(foodTypeSkillId, 1), targets);
owner.setTarget(oldTarget);
}
else
{
// if the owner has no food, the beast immediately despawns, except when it was only
// newly spawned. Newly spawned beasts can last up to 5 minutes
if (_tamedBeast.getRemainingTime() < (MAX_DURATION - 300000))
{
_tamedBeast.setRemainingTime(-1);
}
}
// There are too many conflicting reports about whether distance from home should be taken into consideration. Disabled for now.
// if (_tamedBeast.isTooFarFromHome())
// _tamedBeast.setRemainingTime(-1);
if (_tamedBeast.getRemainingTime() <= 0)
{
_tamedBeast.deleteMe();
}
}
}
}
private class CheckOwnerBuffs implements Runnable
{
private final L2TamedBeastInstance _tamedBeast;
private final int _numBuffs;
CheckOwnerBuffs(L2TamedBeastInstance tamedBeast, int numBuffs)
{
_tamedBeast = tamedBeast;
_numBuffs = numBuffs;
}
@Override
public void run()
{
L2PcInstance owner = _tamedBeast.getOwner();
// check if the owner is no longer around...if so, despawn
if ((owner == null) || !owner.isOnline())
{
deleteMe();
return;
}
// if the owner is too far away, stop anything else and immediately run towards the owner.
if (!isInsideRadius(owner, MAX_DISTANCE_FROM_OWNER, true, true))
{
getAI().startFollow(owner);
return;
}
// if the owner is dead, do nothing...
if (owner.isDead())
{
return;
}
// if the tamed beast is currently casting a spell, do not interfere (do not attempt to cast anything new yet).
if (isCastingNow())
{
return;
}
int totalBuffsOnOwner = 0;
int i = 0;
int rand = Rnd.get(_numBuffs);
Skill buffToGive = null;
// get this npc's skills: getSkills()
for (Skill skill : _tamedBeast.getTemplate().getSkills().values())
{
// if the skill is a buff, check if the owner has it already [ owner.getEffect(L2Skill skill) ]
if (skill.isContinuous() && !skill.isDebuff())
{
if (i++ == rand)
{
buffToGive = skill;
}
if (owner.isAffectedBySkill(skill.getId()))
{
totalBuffsOnOwner++;
}
}
}
// if the owner has less than 60% of this beast's available buff, cast a random buff
if (((_numBuffs * 2) / 3) > totalBuffsOnOwner)
{
_tamedBeast.sitCastAndFollow(buffToGive, owner);
}
getAI().setIntention(CtrlIntention.AI_INTENTION_FOLLOW, _tamedBeast.getOwner());
}
}
@Override
public void onAction(L2PcInstance player, boolean interact)
{
if ((player == null) || !canTarget(player))
{
return;
}
// Check if the L2PcInstance already target the L2NpcInstance
if (this != player.getTarget())
{
// Set the target of the L2PcInstance player
player.setTarget(this);
}
else if (interact)
{
if (isAutoAttackable(player) && (Math.abs(player.getZ() - getZ()) < 100))
{
player.getAI().setIntention(CtrlIntention.AI_INTENTION_ATTACK, this);
}
else
{
// Send a Server->Client ActionFailed to the L2PcInstance in order to avoid that the client wait another packet
player.sendPacket(ActionFailed.STATIC_PACKET);
}
}
}
}