/*
* This program is free software: you can redistribute it and/or modify it under
* the terms of the GNU General Public License as published by the Free Software
* Foundation, either version 3 of the License, or (at your option) any later
* version.
*
* This program is distributed in the hope that it will be useful, but WITHOUT
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
* FOR A PARTICULAR PURPOSE. See the GNU General Public License for more
* details.
*
* You should have received a copy of the GNU General Public License along with
* this program. If not, see <http://www.gnu.org/licenses/>.
*/
package com.l2jserver.gameserver.model.actor.instance;
import static com.l2jserver.gameserver.ai.CtrlIntention.AI_INTENTION_IDLE;
import java.util.List;
import java.util.concurrent.Future;
import javolution.util.FastList;
import javolution.util.FastMap;
import com.l2jserver.gameserver.ThreadPoolManager;
import com.l2jserver.gameserver.ai.CtrlIntention;
import com.l2jserver.gameserver.datatables.SkillTable;
import com.l2jserver.gameserver.model.L2ItemInstance;
import com.l2jserver.gameserver.model.L2Object;
import com.l2jserver.gameserver.model.L2Skill;
import com.l2jserver.gameserver.model.actor.L2Character;
import com.l2jserver.gameserver.network.serverpackets.AbstractNpcInfo;
import com.l2jserver.gameserver.network.serverpackets.ActionFailed;
import com.l2jserver.gameserver.network.serverpackets.MyTargetSelected;
import com.l2jserver.gameserver.network.serverpackets.SocialAction;
import com.l2jserver.gameserver.network.serverpackets.StatusUpdate;
import com.l2jserver.gameserver.network.serverpackets.StopMove;
import com.l2jserver.gameserver.network.serverpackets.ValidateLocation;
import com.l2jserver.gameserver.templates.chars.L2NpcTemplate;
import com.l2jserver.gameserver.templates.skills.L2SkillType;
import com.l2jserver.gameserver.util.Point3D;
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;
private L2PcInstance _owner;
private Future<?> _buffTask = null;
private Future<?> _durationCheckTask = null;
private static boolean _isFreyaBeast;
private List<L2Skill> _beastSkills = null;
public L2TamedBeastInstance(int objectId, L2NpcTemplate template)
{
super(objectId, template);
setInstanceType(InstanceType.L2TamedBeastInstance);
setHome(this);
}
public L2TamedBeastInstance(int objectId, L2NpcTemplate template, L2PcInstance owner, int foodSkillId, int x, int y, int z)
{
super(objectId, template);
_isFreyaBeast = false;
setInstanceType(InstanceType.L2TamedBeastInstance);
setCurrentHp(getMaxHp());
setCurrentMp(getMaxMp());
setOwner(owner);
setFoodType(foodSkillId);
setHome(x,y,z);
this.spawnMe(x, y, z);
}
public L2TamedBeastInstance(int objectId, L2NpcTemplate template, L2PcInstance owner, int food, int x, int y, int z, boolean isFreyaBeast)
{
super(objectId, template);
_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 Point3D getHome()
{
return new Point3D(_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(L2Skill skill)
{
if (_beastSkills == null)
_beastSkills = new FastList<L2Skill>();
_beastSkills.add(skill);
}
public void castBeastSkills()
{
if (_owner == null || _beastSkills == null)
return;
int delay = 100;
for(L2Skill 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 L2Skill _skill;
public buffCast(L2Skill skill)
{
_skill = skill;
}
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 AbstractNpcInfo.NpcInfo(this, owner) );
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 (L2Skill skill: getTemplate().getSkills().values())
{
// if the skill is a buff, check if the owner has it already [ owner.getEffect(L2Skill skill) ]
if (skill.getSkillType() == L2SkillType.BUFF)
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 !(this.isInsideRadius(_homeX, _homeY, _homeZ, MAX_DISTANCE_FROM_HOME, true, true));
}
@Override
public void 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
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)
{
FastMap<Integer, L2Skill> skills = (FastMap<Integer, L2Skill>) getTemplate().getSkills();
for (L2Skill skill: skills.values())
{
// if the skill is a debuff, check if the attacker has it already [ attacker.getEffect(L2Skill skill) ]
if ((skill.getSkillType() == L2SkillType.DEBUFF) && Rnd.get(3) < 1 && (attacker != null && attacker.getFirstEffect(skill) != null))
{
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
FastMap<Integer, L2Skill> skills = (FastMap<Integer, L2Skill>) getTemplate().getSkills();
for (L2Skill skill: skills.values())
{
// if the skill is a buff, check if the owner has it already [ owner.getEffect(L2Skill skill) ]
if ( (Rnd.get(5) < chance) && ((skill.getSkillType() == L2SkillType.HEAL) ||
(skill.getSkillType() == L2SkillType.HOT) ||
(skill.getSkillType() == L2SkillType.BALANCE_LIFE) ||
(skill.getSkillType() == L2SkillType.HEAL_PERCENT) ||
(skill.getSkillType() == L2SkillType.HEAL_STATIC) ||
(skill.getSkillType() == L2SkillType.COMBATPOINTHEAL) ||
(skill.getSkillType() == L2SkillType.CPHOT) ||
(skill.getSkillType() == L2SkillType.MANAHEAL) ||
(skill.getSkillType() == L2SkillType.MANA_BY_LEVEL) ||
(skill.getSkillType() == L2SkillType.MANAHEAL_PERCENT) ||
(skill.getSkillType() == L2SkillType.MANARECHARGE) ||
(skill.getSkillType() == L2SkillType.MPHOT) )
)
{
sitCastAndFollow(skill, _owner);
return;
}
}
}
}
/**
* Prepare and cast a skill:
* First smoothly prepare the beast for casting, by abandoning other actions
* Next, call super.doCast(skill) in order to actually cast the spell
* Finally, return to auto-following the owner.
*
* @see com.l2jserver.gameserver.model.actor.L2Character#doCast(com.l2jserver.gameserver.model.L2Skill)
*/
protected void sitCastAndFollow(L2Skill 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 L2TamedBeastInstance _tamedBeast;
CheckDuration(L2TamedBeastInstance tamedBeast)
{
_tamedBeast = tamedBeast;
}
public void run()
{
int foodTypeSkillId = _tamedBeast.getFoodType();
L2PcInstance owner = _tamedBeast.getOwner();
L2ItemInstance item = null;
if (_isFreyaBeast)
{
item = owner.getInventory().getItemByItemId(foodTypeSkillId);
if (item != null && item.getCount() >= 1)
{
owner.destroyItem("BeastMob", item, 1, _tamedBeast, true);
_tamedBeast.broadcastPacket(new SocialAction(_tamedBeast, 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(SkillTable.getInstance().getInfo(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 L2TamedBeastInstance _tamedBeast;
private int _numBuffs;
CheckOwnerBuffs(L2TamedBeastInstance tamedBeast, int numBuffs)
{
_tamedBeast = tamedBeast;
_numBuffs = numBuffs;
}
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);
L2Skill buffToGive = null;
// get this npc's skills: getSkills()
FastMap<Integer, L2Skill> skills = (FastMap<Integer, L2Skill>) _tamedBeast.getTemplate().getSkills();
for (L2Skill skill: skills.values())
{
// if the skill is a buff, check if the owner has it already [ owner.getEffect(L2Skill skill) ]
if (skill.getSkillType() == L2SkillType.BUFF)
{
if (i++==rand)
buffToGive = skill;
if(owner.getFirstEffect(skill) != null)
{
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);
// Send a Server->Client packet MyTargetSelected to the L2PcInstance player
MyTargetSelected my = new MyTargetSelected(getObjectId(), player.getLevel() - getLevel());
player.sendPacket(my);
// Send a Server->Client packet StatusUpdate of the L2NpcInstance to the L2PcInstance to update its HP bar
StatusUpdate su = new StatusUpdate(this);
su.addAttribute(StatusUpdate.CUR_HP, (int)getStatus().getCurrentHp() );
su.addAttribute(StatusUpdate.MAX_HP, getMaxHp() );
player.sendPacket(su);
// Send a Server->Client packet ValidateLocation to correct the L2NpcInstance position and heading on the client
player.sendPacket(new ValidateLocation(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);
}
}
}
}