923 lines
30 KiB
Java
923 lines
30 KiB
Java
/*
|
|
* This file is part of the L2J Mobius project.
|
|
*
|
|
* This program is free software: you can redistribute it and/or modify
|
|
* it under the terms of the GNU General Public License as published by
|
|
* the Free Software Foundation, either version 3 of the License, or
|
|
* (at your option) any later version.
|
|
*
|
|
* This program is distributed in the hope that it will be useful,
|
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
|
|
* General Public License for more details.
|
|
*
|
|
* You should have received a copy of the GNU General Public License
|
|
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
|
*/
|
|
package com.l2jmobius.gameserver.model.skills;
|
|
|
|
import static com.l2jmobius.gameserver.ai.CtrlIntention.AI_INTENTION_ATTACK;
|
|
|
|
import java.lang.ref.WeakReference;
|
|
import java.util.Collection;
|
|
import java.util.Collections;
|
|
import java.util.List;
|
|
import java.util.Objects;
|
|
import java.util.concurrent.ScheduledFuture;
|
|
import java.util.logging.Level;
|
|
import java.util.logging.Logger;
|
|
|
|
import com.l2jmobius.Config;
|
|
import com.l2jmobius.commons.util.Rnd;
|
|
import com.l2jmobius.gameserver.ThreadPoolManager;
|
|
import com.l2jmobius.gameserver.ai.CtrlEvent;
|
|
import com.l2jmobius.gameserver.ai.CtrlIntention;
|
|
import com.l2jmobius.gameserver.data.xml.impl.ActionData;
|
|
import com.l2jmobius.gameserver.datatables.ItemTable;
|
|
import com.l2jmobius.gameserver.enums.ItemSkillType;
|
|
import com.l2jmobius.gameserver.enums.StatusUpdateType;
|
|
import com.l2jmobius.gameserver.model.L2Object;
|
|
import com.l2jmobius.gameserver.model.L2World;
|
|
import com.l2jmobius.gameserver.model.PcCondOverride;
|
|
import com.l2jmobius.gameserver.model.actor.L2Attackable;
|
|
import com.l2jmobius.gameserver.model.actor.L2Character;
|
|
import com.l2jmobius.gameserver.model.actor.L2Npc;
|
|
import com.l2jmobius.gameserver.model.actor.L2Summon;
|
|
import com.l2jmobius.gameserver.model.actor.instance.L2PcInstance;
|
|
import com.l2jmobius.gameserver.model.effects.L2EffectType;
|
|
import com.l2jmobius.gameserver.model.events.EventDispatcher;
|
|
import com.l2jmobius.gameserver.model.events.impl.character.OnCreatureSkillFinishCast;
|
|
import com.l2jmobius.gameserver.model.events.impl.character.OnCreatureSkillUse;
|
|
import com.l2jmobius.gameserver.model.events.impl.character.npc.OnNpcSkillSee;
|
|
import com.l2jmobius.gameserver.model.events.returns.TerminateReturn;
|
|
import com.l2jmobius.gameserver.model.holders.ItemSkillHolder;
|
|
import com.l2jmobius.gameserver.model.holders.SkillUseHolder;
|
|
import com.l2jmobius.gameserver.model.items.L2Item;
|
|
import com.l2jmobius.gameserver.model.items.L2Weapon;
|
|
import com.l2jmobius.gameserver.model.items.instance.L2ItemInstance;
|
|
import com.l2jmobius.gameserver.model.options.OptionsSkillHolder;
|
|
import com.l2jmobius.gameserver.model.options.OptionsSkillType;
|
|
import com.l2jmobius.gameserver.model.skills.targets.TargetType;
|
|
import com.l2jmobius.gameserver.model.stats.Formulas;
|
|
import com.l2jmobius.gameserver.model.zone.ZoneId;
|
|
import com.l2jmobius.gameserver.network.SystemMessageId;
|
|
import com.l2jmobius.gameserver.network.serverpackets.ActionFailed;
|
|
import com.l2jmobius.gameserver.network.serverpackets.ExRotation;
|
|
import com.l2jmobius.gameserver.network.serverpackets.FlyToLocation;
|
|
import com.l2jmobius.gameserver.network.serverpackets.MagicSkillCanceld;
|
|
import com.l2jmobius.gameserver.network.serverpackets.MagicSkillLaunched;
|
|
import com.l2jmobius.gameserver.network.serverpackets.MagicSkillUse;
|
|
import com.l2jmobius.gameserver.network.serverpackets.MoveToPawn;
|
|
import com.l2jmobius.gameserver.network.serverpackets.SetupGauge;
|
|
import com.l2jmobius.gameserver.network.serverpackets.StatusUpdate;
|
|
import com.l2jmobius.gameserver.network.serverpackets.SystemMessage;
|
|
import com.l2jmobius.gameserver.util.Util;
|
|
|
|
/**
|
|
* @author Nik
|
|
*/
|
|
public class SkillCaster implements Runnable
|
|
{
|
|
private static final Logger LOGGER = Logger.getLogger(SkillCaster.class.getName());
|
|
|
|
private final WeakReference<L2Character> _caster;
|
|
private final WeakReference<L2Object> _target;
|
|
private final Skill _skill;
|
|
private final L2ItemInstance _item;
|
|
private final SkillCastingType _castingType;
|
|
private final int _castTime;
|
|
private int _coolTime;
|
|
private Collection<L2Object> _targets;
|
|
private ScheduledFuture<?> _task;
|
|
private int _phase;
|
|
|
|
private SkillCaster(L2Character caster, L2Object target, Skill skill, L2ItemInstance item, SkillCastingType castingType, boolean ctrlPressed, boolean shiftPressed, int castTime)
|
|
{
|
|
Objects.requireNonNull(caster);
|
|
Objects.requireNonNull(skill);
|
|
Objects.requireNonNull(castingType);
|
|
|
|
_caster = new WeakReference<>(caster);
|
|
_target = new WeakReference<>(target);
|
|
_skill = skill;
|
|
_item = item;
|
|
_castingType = castingType;
|
|
_castTime = castTime;
|
|
}
|
|
|
|
/**
|
|
* Checks if the caster can cast the specified skill on the given target with the selected parameters.
|
|
* @param caster the creature trying to cast
|
|
* @param target the selected target for cast
|
|
* @param skill the skill being cast
|
|
* @param item the reference item which requests the skill cast
|
|
* @param castingType the type of casting
|
|
* @param ctrlPressed force casting
|
|
* @param shiftPressed dont move while casting
|
|
* @return {@code SkillCaster} object containing casting data if casting has started or {@code null} if casting was not started.
|
|
*/
|
|
public static SkillCaster castSkill(L2Character caster, L2Object target, Skill skill, L2ItemInstance item, SkillCastingType castingType, boolean ctrlPressed, boolean shiftPressed)
|
|
{
|
|
return castSkill(caster, target, skill, item, castingType, ctrlPressed, shiftPressed, -1);
|
|
}
|
|
|
|
/**
|
|
* Checks if the caster can cast the specified skill on the given target with the selected parameters.
|
|
* @param caster the creature trying to cast
|
|
* @param target the selected target for cast
|
|
* @param skill the skill being cast
|
|
* @param item the reference item which requests the skill cast
|
|
* @param castingType the type of casting
|
|
* @param ctrlPressed force casting
|
|
* @param shiftPressed dont move while casting
|
|
* @param castTime custom cast time in milliseconds or -1 for default.
|
|
* @return {@code SkillCaster} object containing casting data if casting has started or {@code null} if casting was not started.
|
|
*/
|
|
public static SkillCaster castSkill(L2Character caster, L2Object target, Skill skill, L2ItemInstance item, SkillCastingType castingType, boolean ctrlPressed, boolean shiftPressed, int castTime)
|
|
{
|
|
if ((caster == null) || (skill == null) || (castingType == null))
|
|
{
|
|
return null;
|
|
}
|
|
|
|
if (!checkUseConditions(caster, skill, castingType))
|
|
{
|
|
return null;
|
|
}
|
|
|
|
// Check true aiming target of the skill.
|
|
target = skill.getTarget(caster, target, ctrlPressed, shiftPressed, false);
|
|
if (target == null)
|
|
{
|
|
return null;
|
|
}
|
|
|
|
castTime = castTime > -1 ? castTime : Formulas.calcHitTime(caster, skill);
|
|
|
|
// Schedule a thread that will execute 500ms before casting time is over (for animation issues and retail handling).
|
|
final SkillCaster skillCaster = new SkillCaster(caster, target, skill, item, castingType, ctrlPressed, shiftPressed, castTime);
|
|
skillCaster.run();
|
|
return skillCaster;
|
|
}
|
|
|
|
@Override
|
|
public void run()
|
|
{
|
|
final boolean instantCast = (_castingType == SkillCastingType.SIMULTANEOUS) || _skill.isAbnormalInstant() || _skill.isWithoutAction() || _skill.isToggle();
|
|
|
|
// Skills with instant cast are never launched.
|
|
if (instantCast)
|
|
{
|
|
triggerCast(_caster.get(), _target.get(), _skill, _item, false);
|
|
return;
|
|
}
|
|
|
|
long nextTaskDelay = 0;
|
|
boolean hasNextPhase = false;
|
|
switch (_phase++)
|
|
{
|
|
case 0: // Start skill casting.
|
|
{
|
|
hasNextPhase = startCasting();
|
|
nextTaskDelay = _castTime;
|
|
break;
|
|
}
|
|
case 1: // Launch the skill.
|
|
{
|
|
hasNextPhase = launchSkill();
|
|
nextTaskDelay = Formulas.SKILL_LAUNCH_TIME;
|
|
break;
|
|
}
|
|
case 2: // Finish launching and apply effects.
|
|
{
|
|
hasNextPhase = finishSkill();
|
|
nextTaskDelay = _coolTime;
|
|
break;
|
|
}
|
|
}
|
|
|
|
// Reschedule next task if we have such.
|
|
if (hasNextPhase)
|
|
{
|
|
_task = ThreadPoolManager.getInstance().scheduleEffect(this, nextTaskDelay);
|
|
}
|
|
else
|
|
{
|
|
// Stop casting if there is no next phase.
|
|
stopCasting(false);
|
|
}
|
|
}
|
|
|
|
public boolean startCasting()
|
|
{
|
|
final L2Character caster = _caster.get();
|
|
final L2Object target = _target.get();
|
|
|
|
if ((caster == null) || (target == null))
|
|
{
|
|
return false;
|
|
}
|
|
|
|
_coolTime = Formulas.calcAtkSpd(caster, _skill, _skill.getCoolTime()); // TODO Get proper fomula of this.
|
|
final int displayedCastTime = _castTime + Formulas.SKILL_LAUNCH_TIME; // For client purposes, it must be displayed to player the skill casting time + launch time.
|
|
final boolean instantCast = (_castingType == SkillCastingType.SIMULTANEOUS) || _skill.isAbnormalInstant() || _skill.isWithoutAction();
|
|
|
|
// Add this SkillCaster to the creature so it can be marked as casting.
|
|
if (!instantCast)
|
|
{
|
|
caster.addSkillCaster(_castingType, this);
|
|
}
|
|
|
|
// Disable the skill during the re-use delay and create a task EnableSkill with Medium priority to enable it at the end of the re-use delay
|
|
int reuseDelay = caster.getStat().getReuseTime(_skill);
|
|
if (reuseDelay > 10)
|
|
{
|
|
if (Formulas.calcSkillMastery(caster, _skill))
|
|
{
|
|
reuseDelay = 100;
|
|
caster.sendPacket(SystemMessageId.A_SKILL_IS_READY_TO_BE_USED_AGAIN);
|
|
}
|
|
|
|
if (reuseDelay > 30000)
|
|
{
|
|
caster.addTimeStamp(_skill, reuseDelay);
|
|
}
|
|
else
|
|
{
|
|
caster.disableSkill(_skill, reuseDelay);
|
|
}
|
|
}
|
|
|
|
// Stop movement when casting. Except instant cast.
|
|
if (!instantCast)
|
|
{
|
|
caster.getAI().clientStopMoving(null);
|
|
}
|
|
|
|
// Reduce talisman mana on skill use
|
|
if ((_skill.getReferenceItemId() > 0) && (ItemTable.getInstance().getTemplate(_skill.getReferenceItemId()).getBodyPart() == L2Item.SLOT_DECO))
|
|
{
|
|
final L2ItemInstance talisman = caster.getInventory().getItems(i -> i.getId() == _skill.getReferenceItemId(), L2ItemInstance::isEquipped).stream().findAny().orElse(null);
|
|
if (talisman != null)
|
|
{
|
|
talisman.decreaseMana(false, talisman.useSkillDisTime());
|
|
}
|
|
}
|
|
|
|
if (target != caster)
|
|
{
|
|
// Face the target
|
|
caster.setHeading(Util.calculateHeadingFrom(caster, target));
|
|
caster.broadcastPacket(new ExRotation(caster.getObjectId(), caster.getHeading())); // TODO: Not sent in retail. Probably moveToPawn is enough
|
|
|
|
// Send MoveToPawn packet to trigger Blue Bubbles on target become Red, but don't do it while (double) casting, because that will screw up animation... some fucked up stuff, right?
|
|
if (caster.isPlayer() && !caster.isCastingNow() && target.isCharacter())
|
|
{
|
|
caster.sendPacket(new MoveToPawn(caster, target, (int) caster.calculateDistance(target, false, false)));
|
|
caster.sendPacket(ActionFailed.STATIC_PACKET);
|
|
}
|
|
}
|
|
|
|
// Stop effects since we started casting. It should be sent before casting bar and mana consume.
|
|
caster.stopEffectsOnAction();
|
|
|
|
// Consume skill initial MP needed for cast. Retail sends it regardless if > 0 or not.
|
|
final int initmpcons = caster.getStat().getMpInitialConsume(_skill);
|
|
if (initmpcons > 0)
|
|
{
|
|
if (initmpcons > caster.getCurrentMp())
|
|
{
|
|
caster.sendPacket(SystemMessageId.NOT_ENOUGH_MP);
|
|
return false;
|
|
}
|
|
|
|
caster.getStatus().reduceMp(initmpcons);
|
|
final StatusUpdate su = new StatusUpdate(caster);
|
|
su.addUpdate(StatusUpdateType.CUR_MP, (int) caster.getCurrentMp());
|
|
caster.sendPacket(su);
|
|
}
|
|
|
|
// Send a packet starting the casting.
|
|
final int actionId = caster.isSummon() ? ActionData.getInstance().getSkillActionId(_skill.getId()) : -1;
|
|
if (!_skill.isNotBroadcastable())
|
|
{
|
|
caster.broadcastPacket(new MagicSkillUse(caster, target, _skill.getDisplayId(), _skill.getDisplayLevel(), displayedCastTime, reuseDelay, _skill.getReuseDelayGroup(), actionId, _castingType));
|
|
}
|
|
|
|
if (caster.isPlayer() && !instantCast)
|
|
{
|
|
// Send a system message to the player.
|
|
caster.sendPacket(_skill.getId() != 2046 ? SystemMessage.getSystemMessage(SystemMessageId.YOU_USE_S1).addSkillName(_skill) : SystemMessage.getSystemMessage(SystemMessageId.SUMMONING_YOUR_PET));
|
|
|
|
// Show the gauge bar for casting.
|
|
caster.sendPacket(new SetupGauge(caster.getObjectId(), SetupGauge.BLUE, displayedCastTime));
|
|
}
|
|
|
|
// Consume the required items. Should happen after use message is displayed and SetupGauge
|
|
if ((_skill.getItemConsumeId() > 0) && (_skill.getItemConsumeCount() > 0) && !caster.destroyItemByItemId(_skill.toString(), _skill.getItemConsumeId(), _skill.getItemConsumeCount(), null, true))
|
|
{
|
|
return false;
|
|
}
|
|
|
|
// Trigger any skill cast start effects.
|
|
if (target.isCharacter())
|
|
{
|
|
_skill.applyEffectScope(EffectScope.START, new BuffInfo(caster, (L2Character) target, _skill, false, _item, null), true, false);
|
|
}
|
|
|
|
// Start channeling if skill is channeling.
|
|
if (_skill.isChanneling() && (_skill.getChannelingSkillId() > 0))
|
|
{
|
|
caster.getSkillChannelizer().startChanneling(_skill);
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
public boolean launchSkill()
|
|
{
|
|
final L2Character caster = _caster.get();
|
|
final L2Object target = _target.get();
|
|
|
|
if ((caster == null) || (target == null))
|
|
{
|
|
return false;
|
|
}
|
|
|
|
// Gather list of affected targets by this skill.
|
|
_targets = _skill.getTargetsAffected(caster, target);
|
|
|
|
// Finish flying by setting the target location after picking targets. Packet is sent before MagicSkillLaunched.
|
|
if (_skill.getFlyType() != null)
|
|
{
|
|
caster.broadcastPacket(new FlyToLocation(caster, target, _skill.getFlyType()));
|
|
caster.setXYZ(target.getX(), target.getY(), target.getZ());
|
|
}
|
|
|
|
// Display animation of launching skill upon targets.
|
|
if (!_skill.isNotBroadcastable())
|
|
{
|
|
caster.broadcastPacket(new MagicSkillLaunched(caster, _skill.getDisplayId(), _skill.getDisplayLevel(), _castingType, _targets));
|
|
}
|
|
return true;
|
|
}
|
|
|
|
public boolean finishSkill()
|
|
{
|
|
final L2Character caster = _caster.get();
|
|
final L2Object target = _target.get();
|
|
|
|
if ((caster == null) || (target == null))
|
|
{
|
|
return false;
|
|
}
|
|
|
|
if (_targets == null)
|
|
{
|
|
_targets = Collections.singletonList(target);
|
|
}
|
|
|
|
final StatusUpdate su = new StatusUpdate(caster);
|
|
|
|
// Consume the required MP or stop casting if not enough.
|
|
final double mpConsume = _skill.getMpConsume() > 0 ? caster.getStat().getMpConsume(_skill) : 0;
|
|
if (mpConsume > 0)
|
|
{
|
|
if (mpConsume > caster.getCurrentMp())
|
|
{
|
|
caster.sendPacket(SystemMessageId.NOT_ENOUGH_MP);
|
|
return false;
|
|
}
|
|
|
|
caster.getStatus().reduceMp(mpConsume);
|
|
su.addUpdate(StatusUpdateType.CUR_MP, (int) caster.getCurrentMp());
|
|
}
|
|
|
|
// Consume the required HP or stop casting if not enough.
|
|
final double consumeHp = _skill.getHpConsume();
|
|
if (consumeHp > 0)
|
|
{
|
|
if (consumeHp >= caster.getCurrentHp())
|
|
{
|
|
caster.sendPacket(SystemMessageId.NOT_ENOUGH_HP);
|
|
return false;
|
|
}
|
|
|
|
caster.getStatus().reduceHp(consumeHp, caster, true);
|
|
su.addUpdate(StatusUpdateType.CUR_HP, (int) caster.getCurrentHp());
|
|
}
|
|
|
|
// Send HP/MP consumption packet if any attribute is set.
|
|
if (su.hasUpdates())
|
|
{
|
|
caster.sendPacket(su);
|
|
}
|
|
|
|
// Consume Souls if necessary
|
|
if (caster.isPlayer() && (_skill.getMaxSoulConsumeCount() > 0) && !caster.getActingPlayer().decreaseSouls(_skill.getMaxSoulConsumeCount(), _skill))
|
|
{
|
|
return false;
|
|
}
|
|
|
|
// Noptify skill is casted.
|
|
EventDispatcher.getInstance().notifyEvent(new OnCreatureSkillFinishCast(caster, target, _skill, _skill.isWithoutAction()), caster);
|
|
|
|
// Call the skill's effects and AI interraction and stuff.
|
|
SkillCaster.callSkill(caster, target, _targets, _skill, _item);
|
|
|
|
// Start attack stance.
|
|
if (!_skill.isWithoutAction())
|
|
{
|
|
if (_skill.isBad() && (_skill.getTargetType() != TargetType.DOOR_TREASURE))
|
|
{
|
|
caster.getAI().clientStartAutoAttack();
|
|
}
|
|
}
|
|
|
|
// Notify DP Scripts
|
|
caster.notifyQuestEventSkillFinished(_skill, target);
|
|
|
|
// On each repeat recharge shots before cast.
|
|
caster.rechargeShots(_skill.useSoulShot(), _skill.useSpiritShot(), false);
|
|
return true;
|
|
}
|
|
|
|
public static void callSkill(L2Character caster, L2Object target, Collection<L2Object> targets, Skill skill, L2ItemInstance item)
|
|
{
|
|
// Launch the magic skill in order to calculate its effects
|
|
try
|
|
{
|
|
// Check if the toggle skill effects are already in progress on the L2Character
|
|
if (skill.isToggle() && caster.isAffectedBySkill(skill.getId()))
|
|
{
|
|
return;
|
|
}
|
|
|
|
// Initial checks
|
|
for (L2Object obj : targets)
|
|
{
|
|
if ((obj == null) || !obj.isCharacter())
|
|
{
|
|
continue;
|
|
}
|
|
|
|
final L2Character creature = (L2Character) obj;
|
|
|
|
// Check raid monster/minion attack and check buffing characters who attack raid monsters. Raid is still affected by skills.
|
|
if (!Config.RAID_DISABLE_CURSE && creature.isRaid() && creature.giveRaidCurse() && (caster.getLevel() >= (creature.getLevel() + 9)))
|
|
{
|
|
if (skill.isBad() || ((creature.getTarget() == caster) && ((L2Attackable) creature).getAggroList().containsKey(caster)))
|
|
{
|
|
// Skills such as Summon Battle Scar too can trigger magic silence.
|
|
final CommonSkill curse = skill.isBad() ? CommonSkill.RAID_CURSE2 : CommonSkill.RAID_CURSE;
|
|
final Skill curseSkill = curse.getSkill();
|
|
if (curseSkill != null)
|
|
{
|
|
curseSkill.applyEffects(creature, caster);
|
|
}
|
|
}
|
|
}
|
|
|
|
// Static skills not trigger any chance skills
|
|
if (!skill.isStatic())
|
|
{
|
|
final L2Weapon activeWeapon = caster.getActiveWeaponItem();
|
|
// Launch weapon Special ability skill effect if available
|
|
if ((activeWeapon != null) && !creature.isDead())
|
|
{
|
|
activeWeapon.applyConditionalSkills(caster, creature, skill, ItemSkillType.ON_MAGIC_SKILL);
|
|
}
|
|
|
|
if (caster.hasTriggerSkills())
|
|
{
|
|
for (OptionsSkillHolder holder : caster.getTriggerSkills().values())
|
|
{
|
|
if ((skill.isMagic() && (holder.getSkillType() == OptionsSkillType.MAGIC)) || (skill.isPhysical() && (holder.getSkillType() == OptionsSkillType.ATTACK)))
|
|
{
|
|
if (Rnd.get(100) < holder.getChance())
|
|
{
|
|
triggerCast(caster, creature, holder.getSkill(), null, false);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// Launch the magic skill and calculate its effects
|
|
skill.activateSkill(caster, item, targets.toArray(new L2Object[0]));
|
|
|
|
final L2PcInstance player = caster.getActingPlayer();
|
|
if (player != null)
|
|
{
|
|
for (L2Object obj : targets)
|
|
{
|
|
if (!(obj instanceof L2Character))
|
|
{
|
|
continue;
|
|
}
|
|
|
|
if (skill.isBad())
|
|
{
|
|
if (obj.isPlayable())
|
|
{
|
|
// Update pvpflag.
|
|
player.updatePvPStatus((L2Character) obj);
|
|
|
|
if (obj.isSummon())
|
|
{
|
|
((L2Summon) obj).updateAndBroadcastStatus(1);
|
|
}
|
|
}
|
|
else if (obj.isAttackable())
|
|
{
|
|
// Add hate to the attackable, and put it in the attack list.
|
|
((L2Attackable) obj).addDamageHate(caster, 0, -skill.getEffectPoint());
|
|
((L2Character) obj).addAttackerToAttackByList(caster);
|
|
}
|
|
|
|
// notify target AI about the attack
|
|
if (((L2Character) obj).hasAI() && !skill.hasEffectType(L2EffectType.HATE))
|
|
{
|
|
((L2Character) obj).getAI().notifyEvent(CtrlEvent.EVT_ATTACKED, caster);
|
|
}
|
|
}
|
|
else if (obj.isMonster() || (obj.isPlayable() && ((obj.getActingPlayer().getPvpFlag() > 0) || (obj.getActingPlayer().getReputation() < 0))))
|
|
{
|
|
// Supporting players or monsters result in pvpflag.
|
|
player.updatePvPStatus();
|
|
}
|
|
}
|
|
|
|
// Mobs in range 1000 see spell
|
|
L2World.getInstance().forEachVisibleObjectInRange(player, L2Npc.class, 1000, npcMob ->
|
|
{
|
|
EventDispatcher.getInstance().notifyEventAsync(new OnNpcSkillSee(npcMob, player, skill, caster.isSummon(), targets.toArray(new L2Object[0])), npcMob);
|
|
|
|
// On Skill See logic
|
|
if (npcMob.isAttackable())
|
|
{
|
|
final L2Attackable attackable = (L2Attackable) npcMob;
|
|
|
|
if (skill.getEffectPoint() > 0)
|
|
{
|
|
if (attackable.hasAI() && (attackable.getAI().getIntention() == AI_INTENTION_ATTACK))
|
|
{
|
|
final L2Object npcTarget = attackable.getTarget();
|
|
for (L2Object skillTarget : targets)
|
|
{
|
|
if ((npcTarget == skillTarget) || (npcMob == skillTarget))
|
|
{
|
|
final L2Character originalCaster = caster.isSummon() ? caster : player;
|
|
attackable.addDamageHate(originalCaster, 0, (skill.getEffectPoint() * 150) / (attackable.getLevel() + 7));
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
});
|
|
}
|
|
}
|
|
catch (Exception e)
|
|
{
|
|
LOGGER.log(Level.WARNING, caster + " callSkill() failed.", e);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Stops this casting and cleans all cast parameters.<br>
|
|
* @param aborted if {@code true}, server will send packets to the player, notifying him that the skill has been aborted.
|
|
*/
|
|
public void stopCasting(boolean aborted)
|
|
{
|
|
// Cancel the task and unset it.
|
|
if (_task != null)
|
|
{
|
|
_task.cancel(false);
|
|
_task = null;
|
|
}
|
|
|
|
final L2Character caster = _caster.get();
|
|
final L2Object target = _target.get();
|
|
if (caster == null)
|
|
{
|
|
return;
|
|
}
|
|
|
|
caster.removeSkillCaster(_castingType);
|
|
|
|
if (caster.isChanneling())
|
|
{
|
|
caster.getSkillChannelizer().stopChanneling();
|
|
}
|
|
|
|
// If aborted, broadcast casting aborted.
|
|
if (aborted)
|
|
{
|
|
caster.broadcastPacket(new MagicSkillCanceld(caster.getObjectId())); // broadcast packet to stop animations client-side
|
|
caster.sendPacket(ActionFailed.get(_castingType)); // send an "action failed" packet to the caster
|
|
}
|
|
|
|
// Notify the AI of the L2Character with EVT_FINISH_CASTING
|
|
caster.getAI().notifyEvent(CtrlEvent.EVT_FINISH_CASTING);
|
|
|
|
// If there is a queued skill, launch it and wipe the queue.
|
|
if (caster.isPlayer())
|
|
{
|
|
final L2PcInstance currPlayer = caster.getActingPlayer();
|
|
final SkillUseHolder queuedSkill = currPlayer.getQueuedSkill();
|
|
|
|
if (queuedSkill != null)
|
|
{
|
|
ThreadPoolManager.getInstance().executeGeneral(() ->
|
|
{
|
|
currPlayer.setQueuedSkill(null, null, false, false);
|
|
currPlayer.useMagic(queuedSkill.getSkill(), queuedSkill.getItem(), queuedSkill.isCtrlPressed(), queuedSkill.isShiftPressed());
|
|
});
|
|
|
|
return;
|
|
}
|
|
}
|
|
|
|
// Attack target after skill use
|
|
// TODO: This shouldnt be here. If skill condition fail, you still go autoattack. This doesn't happen if skill is in cooldown though.
|
|
if ((_skill.nextActionIsAttack()) && (target != null) && (target != caster) && target.canBeAttacked())
|
|
{
|
|
if ((caster.getAI().getNextIntention() == null) || (caster.getAI().getNextIntention().getCtrlIntention() != CtrlIntention.AI_INTENTION_MOVE_TO))
|
|
{
|
|
caster.getAI().setIntention(CtrlIntention.AI_INTENTION_ATTACK, target);
|
|
}
|
|
}
|
|
}
|
|
|
|
public static void triggerCast(L2Character activeChar, L2Character target, Skill skill)
|
|
{
|
|
triggerCast(activeChar, target, skill, null, true);
|
|
}
|
|
|
|
public static void triggerCast(L2Character activeChar, L2Object target, Skill skill, L2ItemInstance item, boolean ignoreTargetType)
|
|
{
|
|
try
|
|
{
|
|
if ((activeChar == null) || (skill == null))
|
|
{
|
|
return;
|
|
}
|
|
|
|
if (skill.checkCondition(activeChar, target))
|
|
{
|
|
if (activeChar.isSkillDisabled(skill))
|
|
{
|
|
return;
|
|
}
|
|
|
|
if (skill.getReuseDelay() > 0)
|
|
{
|
|
activeChar.disableSkill(skill, skill.getReuseDelay());
|
|
}
|
|
|
|
if (!ignoreTargetType)
|
|
{
|
|
final L2Object objTarget = skill.getTarget(activeChar, false, false, false);
|
|
if (objTarget.isCharacter())
|
|
{
|
|
target = objTarget;
|
|
}
|
|
}
|
|
|
|
final L2Object[] targets = skill.getTargetsAffected(activeChar, target).toArray(new L2Object[0]);
|
|
|
|
if (!skill.isNotBroadcastable())
|
|
{
|
|
activeChar.broadcastPacket(new MagicSkillUse(activeChar, target, skill.getDisplayId(), skill.getLevel(), 0, 0));
|
|
}
|
|
|
|
// Launch the magic skill and calculate its effects
|
|
skill.activateSkill(activeChar, item, targets);
|
|
}
|
|
}
|
|
catch (Exception e)
|
|
{
|
|
LOGGER.log(Level.WARNING, "Failed simultaneous cast: ", e);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* @return the skill that is casting.
|
|
*/
|
|
public Skill getSkill()
|
|
{
|
|
return _skill;
|
|
}
|
|
|
|
/**
|
|
* @return the creature casting the skill.
|
|
*/
|
|
public L2Character getCaster()
|
|
{
|
|
return _caster.get();
|
|
}
|
|
|
|
/**
|
|
* @return the target this skill is being cast on.
|
|
*/
|
|
public L2Object getTarget()
|
|
{
|
|
return _target.get();
|
|
}
|
|
|
|
/**
|
|
* @return the item that has been used in this casting.
|
|
*/
|
|
public L2ItemInstance getItem()
|
|
{
|
|
return _item;
|
|
}
|
|
|
|
/**
|
|
* @return {@code true} if casting can be aborted through regular means such as cast break while being attacked or while cancelling target, {@code false} otherwise.
|
|
*/
|
|
public boolean canAbortCast()
|
|
{
|
|
return _targets == null; // When targets are allocated, that means skill is already launched, therefore cannot be aborted.
|
|
}
|
|
|
|
/**
|
|
* @return the type of this caster, which also defines the casting display bar on the player.
|
|
*/
|
|
public SkillCastingType getCastingType()
|
|
{
|
|
return _castingType;
|
|
}
|
|
|
|
public boolean isNormalFirstType()
|
|
{
|
|
return _castingType == SkillCastingType.NORMAL;
|
|
}
|
|
|
|
public boolean isNormalSecondType()
|
|
{
|
|
return _castingType == SkillCastingType.NORMAL_SECOND;
|
|
}
|
|
|
|
public boolean isAnyNormalType()
|
|
{
|
|
return (_castingType == SkillCastingType.NORMAL) || (_castingType == SkillCastingType.NORMAL_SECOND);
|
|
}
|
|
|
|
@Override
|
|
public String toString()
|
|
{
|
|
return super.toString() + " [caster: " + String.valueOf(_caster.get()) + " skill: " + String.valueOf(_skill) + " target: " + String.valueOf(_target.get()) + " type: " + String.valueOf(_castingType) + "]";
|
|
}
|
|
|
|
/**
|
|
* Checks general conditions for casting a skill through the regular casting type.
|
|
* @param caster the caster checked if can cast the given skill.
|
|
* @param skill the skill to be check if it can be casted by the given caster or not.
|
|
* @return {@code true} if the caster can proceed with casting the given skill, {@code false} otherwise.
|
|
*/
|
|
public static boolean checkUseConditions(L2Character caster, Skill skill)
|
|
{
|
|
return checkUseConditions(caster, skill, SkillCastingType.NORMAL);
|
|
}
|
|
|
|
/**
|
|
* Checks general conditions for casting a skill.
|
|
* @param caster the caster checked if can cast the given skill.
|
|
* @param skill the skill to be check if it can be casted by the given caster or not.
|
|
* @param castingType used to check if caster is currently casting this type of cast.
|
|
* @return {@code true} if the caster can proceed with casting the given skill, {@code false} otherwise.
|
|
*/
|
|
public static boolean checkUseConditions(L2Character caster, Skill skill, SkillCastingType castingType)
|
|
{
|
|
if (caster == null)
|
|
{
|
|
return false;
|
|
}
|
|
|
|
if ((skill == null) || caster.isSkillDisabled(skill) || (((skill.getFlyRadius() > 0) || (skill.getFlyType() != null)) && caster.isMovementDisabled()))
|
|
{
|
|
caster.sendPacket(ActionFailed.STATIC_PACKET);
|
|
return false;
|
|
}
|
|
|
|
final TerminateReturn term = EventDispatcher.getInstance().notifyEvent(new OnCreatureSkillUse(caster, skill, skill.isWithoutAction()), caster, TerminateReturn.class);
|
|
if ((term != null) && term.terminate())
|
|
{
|
|
caster.sendPacket(ActionFailed.STATIC_PACKET);
|
|
return false;
|
|
}
|
|
|
|
// Check if creature is already casting
|
|
if ((castingType != null) && caster.isCastingNow(castingType))
|
|
{
|
|
caster.sendPacket(ActionFailed.get(castingType));
|
|
return false;
|
|
}
|
|
|
|
// Check if the caster has enough MP
|
|
if (caster.getCurrentMp() < (caster.getStat().getMpConsume(skill) + caster.getStat().getMpInitialConsume(skill)))
|
|
{
|
|
caster.sendPacket(SystemMessageId.NOT_ENOUGH_MP);
|
|
caster.sendPacket(ActionFailed.STATIC_PACKET);
|
|
return false;
|
|
}
|
|
|
|
// Check if the caster has enough HP
|
|
if (caster.getCurrentHp() <= skill.getHpConsume())
|
|
{
|
|
caster.sendPacket(SystemMessageId.NOT_ENOUGH_HP);
|
|
caster.sendPacket(ActionFailed.STATIC_PACKET);
|
|
return false;
|
|
}
|
|
|
|
// Skill mute checks.
|
|
if (!skill.isStatic())
|
|
{
|
|
// Check if the skill is a magic spell and if the L2Character is not muted
|
|
if (skill.isMagic())
|
|
{
|
|
if (caster.isMuted())
|
|
{
|
|
caster.sendPacket(ActionFailed.STATIC_PACKET);
|
|
return false;
|
|
}
|
|
}
|
|
else if (caster.isPhysicalMuted()) // Check if the skill is physical and if the L2Character is not physical_muted
|
|
{
|
|
caster.sendPacket(ActionFailed.STATIC_PACKET);
|
|
return false;
|
|
}
|
|
}
|
|
|
|
// Check if the caster's weapon is limited to use only its own skills
|
|
final L2Weapon weapon = caster.getActiveWeaponItem();
|
|
if ((weapon != null) && weapon.useWeaponSkillsOnly() && !caster.canOverrideCond(PcCondOverride.SKILL_CONDITIONS))
|
|
{
|
|
final List<ItemSkillHolder> weaponSkills = weapon.getSkills(ItemSkillType.NORMAL);
|
|
if ((weaponSkills != null) && !weaponSkills.stream().anyMatch(sh -> sh.getSkillId() == skill.getId()))
|
|
{
|
|
caster.sendPacket(SystemMessageId.THAT_WEAPON_CANNOT_USE_ANY_OTHER_SKILL_EXCEPT_THE_WEAPON_S_SKILL);
|
|
return false;
|
|
}
|
|
}
|
|
|
|
// Check if the spell consumes an Item
|
|
// TODO: combine check and consume
|
|
if ((skill.getItemConsumeId() > 0) && (skill.getItemConsumeCount() > 0) && (caster.getInventory() != null))
|
|
{
|
|
// Get the L2ItemInstance consumed by the spell
|
|
final L2ItemInstance requiredItems = caster.getInventory().getItemByItemId(skill.getItemConsumeId());
|
|
if ((requiredItems == null) || (requiredItems.getCount() < skill.getItemConsumeCount()))
|
|
{
|
|
// Checked: when a summon skill failed, server show required consume item count
|
|
if (skill.hasEffectType(L2EffectType.SUMMON))
|
|
{
|
|
final SystemMessage sm = SystemMessage.getSystemMessage(SystemMessageId.SUMMONING_A_SERVITOR_COSTS_S2_S1);
|
|
sm.addItemName(skill.getItemConsumeId());
|
|
sm.addInt(skill.getItemConsumeCount());
|
|
caster.sendPacket(sm);
|
|
}
|
|
else
|
|
{
|
|
caster.sendPacket(SystemMessageId.THERE_ARE_NOT_ENOUGH_NECESSARY_ITEMS_TO_USE_THE_SKILL);
|
|
}
|
|
return false;
|
|
}
|
|
}
|
|
|
|
if (caster.isPlayer())
|
|
{
|
|
final L2PcInstance player = caster.getActingPlayer();
|
|
if (player.inObserverMode())
|
|
{
|
|
return false;
|
|
}
|
|
|
|
if (player.isInOlympiadMode() && skill.isBlockedInOlympiad())
|
|
{
|
|
player.sendPacket(SystemMessageId.YOU_CANNOT_USE_THAT_SKILL_IN_A_OLYMPIAD_MATCH);
|
|
return false;
|
|
}
|
|
|
|
if (player.isInsideZone(ZoneId.SAYUNE))
|
|
{
|
|
player.sendPacket(SystemMessageId.YOU_CANNOT_USE_SKILLS_IN_THE_CORRESPONDING_REGION);
|
|
return false;
|
|
}
|
|
|
|
// Check if not in AirShip
|
|
if (player.isInAirShip() && !skill.hasEffectType(L2EffectType.REFUEL_AIRSHIP))
|
|
{
|
|
final SystemMessage sm = SystemMessage.getSystemMessage(SystemMessageId.S1_CANNOT_BE_USED_DUE_TO_UNSUITABLE_TERMS);
|
|
sm.addSkillName(skill);
|
|
player.sendPacket(sm);
|
|
return false;
|
|
}
|
|
}
|
|
|
|
return true;
|
|
}
|
|
}
|