테스트

aion-server 4.8

Gitteol
최고관리자 · 1 · 💬 0 클론/새로받기
 4.8 61f661d · 1 commits 새로받기(Pull)
game-server/src/com/aionemu/gameserver/skillengine/model/Skill.java
package com.aionemu.gameserver.skillengine.model;

import java.util.ArrayList;
import java.util.List;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import com.aionemu.commons.utils.Rnd;
import com.aionemu.gameserver.ai.AISubState;
import com.aionemu.gameserver.ai.NpcAI;
import com.aionemu.gameserver.ai.event.AIEventType;
import com.aionemu.gameserver.ai.handler.ShoutEventHandler;
import com.aionemu.gameserver.ai.manager.SkillAttackManager;
import com.aionemu.gameserver.configs.main.CustomConfig;
import com.aionemu.gameserver.configs.main.GSConfig;
import com.aionemu.gameserver.configs.main.SecurityConfig;
import com.aionemu.gameserver.controllers.attack.AttackStatus;
import com.aionemu.gameserver.controllers.observer.ActionObserver;
import com.aionemu.gameserver.controllers.observer.ObserverType;
import com.aionemu.gameserver.controllers.observer.StartMovingListener;
import com.aionemu.gameserver.dataholders.DataManager;
import com.aionemu.gameserver.dataholders.MotionData.AnimationTimes;
import com.aionemu.gameserver.model.PlayerClass;
import com.aionemu.gameserver.model.gameobjects.Creature;
import com.aionemu.gameserver.model.gameobjects.Item;
import com.aionemu.gameserver.model.gameobjects.Npc;
import com.aionemu.gameserver.model.gameobjects.player.Player;
import com.aionemu.gameserver.model.skill.NpcSkillEntry;
import com.aionemu.gameserver.model.stats.container.StatEnum;
import com.aionemu.gameserver.model.templates.item.ItemTemplate;
import com.aionemu.gameserver.network.aion.serverpackets.SM_CASTSPELL;
import com.aionemu.gameserver.network.aion.serverpackets.SM_CASTSPELL_RESULT;
import com.aionemu.gameserver.network.aion.serverpackets.SM_ITEM_USAGE_ANIMATION;
import com.aionemu.gameserver.network.aion.serverpackets.SM_SYSTEM_MESSAGE;
import com.aionemu.gameserver.questEngine.QuestEngine;
import com.aionemu.gameserver.questEngine.model.QuestEnv;
import com.aionemu.gameserver.services.abyss.AbyssService;
import com.aionemu.gameserver.services.item.ItemPacketService.ItemUpdateType;
import com.aionemu.gameserver.skillengine.SkillEngine;
import com.aionemu.gameserver.skillengine.action.Action;
import com.aionemu.gameserver.skillengine.action.Actions;
import com.aionemu.gameserver.skillengine.condition.Conditions;
import com.aionemu.gameserver.skillengine.condition.SkillChargeCondition;
import com.aionemu.gameserver.skillengine.effect.AbnormalState;
import com.aionemu.gameserver.skillengine.properties.FirstTargetAttribute;
import com.aionemu.gameserver.skillengine.properties.Properties;
import com.aionemu.gameserver.skillengine.properties.Properties.CastState;
import com.aionemu.gameserver.skillengine.properties.TargetRangeAttribute;
import com.aionemu.gameserver.skillengine.properties.TargetRelationAttribute;
import com.aionemu.gameserver.utils.PacketSendUtility;
import com.aionemu.gameserver.utils.PositionUtil;
import com.aionemu.gameserver.utils.ThreadPoolManager;
import com.aionemu.gameserver.utils.audit.AuditLogger;

/**
 * @author ATracer, Wakizashi, Neon
 */
public class Skill {

	private static final Logger log = LoggerFactory.getLogger(Skill.class);

	private final List<Creature> effectedList;
	private Creature firstTarget;
	protected final Creature effector;
	private final int skillLevel;
	protected SkillMethod skillMethod;
	protected final StartMovingListener moveListener;
	private final SkillTemplate skillTemplate;
	private boolean firstTargetRangeCheck = true;
	private final ItemTemplate itemTemplate;
	private int itemObjectId = 0;
	private int targetType;
	private boolean chainSuccess = true;
	private boolean isCancelled = false;
	private boolean blockedPenaltySkill = false;
	private float x;
	private float y;
	private float z;
	private byte h;
	private int boostSkillCost;
	/**
	 * Duration that depends on BOOST_CASTING_TIME
	 */
	private int baseCastDuration;
	private int castDuration;
	private int clientHitTime; // from CM_CASTSPELL
	private int hitTime; // time when effect is applied
	private float castSpeedForAnimationBoostAndChargeSkills; // cast speed can boost the animation time of the current skill and the hit time of the following skill
	private long castStartTime;
	private String chainCategory = null;
	private int chainUsageDuration = 0;
	private int hate;
	private volatile ActionObserver firstTargetDieObserver;

	public enum SkillMethod {
		CAST,
		ITEM,
		PASSIVE,
		PROVOKED,
		PENALTY
	}

	/**
	 * Each skill is a separate object upon invocation Skill level will be populated from player SkillList
	 */
	public Skill(SkillTemplate skillTemplate, Player effector, Creature firstTarget) {
		this(skillTemplate, effector, effector.getSkillList().getSkillLevel(skillTemplate.getSkillId()), firstTarget, null);
	}

	public Skill(SkillTemplate skillTemplate, Player effector, Creature firstTarget, int skillLevel) {
		this(skillTemplate, effector, skillLevel, firstTarget, null);
	}

	public Skill(SkillTemplate skillTemplate, Creature effector, int skillLvl, Creature firstTarget, ItemTemplate itemTemplate) {
		this.effectedList = new ArrayList<>();
		this.moveListener = new StartMovingListener();
		this.firstTarget = firstTarget;
		this.skillLevel = skillLvl;
		this.skillTemplate = skillTemplate;
		this.effector = effector;
		this.baseCastDuration = skillTemplate.getDuration();
		this.castDuration = skillTemplate.getDuration();
		this.itemTemplate = itemTemplate;
		initializeSkillMethod();
	}

	protected void initializeSkillMethod() {
		if (itemTemplate != null)
			skillMethod = SkillMethod.ITEM;
		else if (skillTemplate.isPassive())
			skillMethod = SkillMethod.PASSIVE;
		else if (skillTemplate.isProvoked())
			skillMethod = SkillMethod.PROVOKED;
		else
			skillMethod = SkillMethod.CAST;
	}

	/**
	 * Check if the skill can be used
	 *
	 * @return True if the skill can be used
	 */
	public boolean canUseSkill(CastState castState) {
		Properties properties = skillTemplate.getProperties();
		if (properties != null && !properties.validate(this, castState)) {
			log.debug("properties failed");
			return false;
		}

		if (!preCastCheck())
			return false;

		// check for counter skill
		if (effector instanceof Player player) {
			if (skillMethod == SkillMethod.CAST && chainCategory == null) // category gets set in preCastCheck()
				player.getChainSkills().resetChain();

			if (skillTemplate.getCounterSkill() != null) {
				long time = player.getLastCounterSkill(skillTemplate.getCounterSkill());
				if ((time + 5000) < System.currentTimeMillis()) {
					log.debug("chain skill failed, too late");
					return false;
				}
			}

			if (skillMethod == SkillMethod.ITEM && baseCastDuration > 0 && player.getMoveController().isInMove()) {
				PacketSendUtility.sendPacket(player, SM_SYSTEM_MESSAGE.STR_ITEM_CANCELED());
				return false;
			}
		}

		return validateEffectedList();
	}

	private boolean validateEffectedList() {
		if (effector instanceof Player player) {
			if (canUseSkill(player))
				effectedList.removeIf(effected -> !isValidTarget(player, effected));
			else
				effectedList.clear();
		}

		if (targetType == 0 && effectedList.isEmpty()) { // target selected but no target will be hit
			if (getTargetRangeAttribute() != TargetRangeAttribute.AREA) { // don't restrict AoE activation
				if (effector instanceof Player player)
					PacketSendUtility.sendPacket(player, SM_SYSTEM_MESSAGE.STR_SKILL_TARGET_IS_NOT_VALID());
				return false;
			}
		}

		return true;
	}

	private boolean canUseSkill(Player player) {
		if (player.isUsingFlyTeleport())
			return false;
		if (!getSkillTemplate().hasEvadeEffect() && player.getEffectController().isInAnyAbnormalState(AbnormalState.CANT_ATTACK_STATE))
			return false;
		if (player.getStore() != null)
			return false;
		return true;
	}

	private boolean isValidTarget(Player player, Creature target) {
		if (target instanceof Player targetPlayer) {
			if (targetPlayer.isUsingFlyTeleport())
				return false;
			if (target.getRace() != player.getRace()) {
				if (!target.isEnemyFrom(player))
					return false;
			} else if (targetPlayer.isDueling(player) && getSkillTemplate().getProperties().getTargetRelation() != TargetRelationAttribute.ENEMY) {
				PacketSendUtility.sendPacket(player, SM_SYSTEM_MESSAGE.STR_SKILL_TARGET_IS_NOT_VALID());
				return false;
			}
		}

		if (target.getLifeStats().isAboutToDie() && !isNonTargetAOE())
			return false;

		if (target.isDead() && !getSkillTemplate().hasResurrectEffect() && !isNonTargetAOE()) {
			PacketSendUtility.sendPacket(player, SM_SYSTEM_MESSAGE.STR_SKILL_TARGET_IS_NOT_VALID());
			return false;
		}

		// cant resurrect non players and non dead
		if (getSkillTemplate().hasResurrectEffect() && (!(target instanceof Player) || !target.isDead()))
			return false;

		return true;
	}

	/**
	 * Skill entry point
	 *
	 * @return true if usage is successful
	 */
	public boolean useSkill() {
		return useSkill(SecurityConfig.CHECK_ANIMATIONS, true);
	}

	public boolean useNoAnimationSkill() {
		return useSkill(false, true);
	}

	public boolean useWithoutPropSkill() {
		return useSkill(false, false);
	}

	private boolean useSkill(boolean checkAnimation, boolean checkproperties) {
		boostSkillCost = 0;
		effector.getObserveController().notifyBoostSkillCostObservers(this);

		if (checkproperties && !canUseSkill(CastState.CAST_START))
			return false;

		updateCastDurationAndSpeed();
		updateHitTime(checkAnimation);

		// notify skill use observers
		if (skillMethod == SkillMethod.CAST || skillMethod == SkillMethod.ITEM)
			effector.getObserveController().notifyStartSkillCastObservers(this);

		// start casting
		effector.setCasting(this);

		// send packets to start casting
		if (skillMethod == SkillMethod.CAST || skillMethod == SkillMethod.ITEM) {
			castStartTime = System.currentTimeMillis();
			startCast();
			if (effector instanceof Npc)
				effector.getAi().setSubStateIfNot(AISubState.CAST);
		}

		effector.getObserveController().attach(moveListener);

		if (effector instanceof Npc npc) {
			NpcSkillEntry currentNpcSkillEntry = npc.getGameStats().getLastSkill();
			if (currentNpcSkillEntry != null) {
				currentNpcSkillEntry.setLastTimeUsed();
				npc.getGameStats().setNextSkillDelay(currentNpcSkillEntry.getNextSkillTime());
			} else {
				npc.getGameStats().setNextSkillDelay(-1);
			}
		}
		effector.getAi().onStartUseSkill(skillTemplate, skillLevel);
		if (skillTemplate.isCharge()) {
			ThreadPoolManager.getInstance().schedule(this::cancelCurrentSkillCast, castDuration);
		} else if (castDuration > 0) {
			ThreadPoolManager.getInstance().schedule(this::endCast, castDuration);
		} else {
			endCast();
		}
		return true;
	}

	private void setCooldowns() {
		int cooldown = effector.getSkillCooldown(skillTemplate);
		if (cooldown != 0) {
			if (skillTemplate.getCooldownDeltaLv() != 0)
				cooldown = cooldown + skillTemplate.getCooldownDeltaLv() * skillLevel;
			effector.setSkillCoolDown(skillTemplate.getCooldownId(), cooldown * 100 + System.currentTimeMillis());
		}
	}

	public int getCooldown() {
		int cooldown = effector.getSkillCooldown(skillTemplate);
		if (cooldown != 0)
			if (skillTemplate.getCooldownDeltaLv() != 0)
				cooldown = cooldown + skillTemplate.getCooldownDeltaLv() * skillLevel;
		return cooldown;
	}

	protected void updateCastDurationAndSpeed() {
		if (effector instanceof Npc npc) { // TODO: check if all skills should be effected
			castDuration = Math.round(baseCastDuration * (npc.getGameStats().getCastSpeed() / 1000f));
			castSpeedForAnimationBoostAndChargeSkills = 1f;
		} else if (skillTemplate.isCharge()) {
			boolean isChargeTimeFixed = updateChargeBaseCastDuration();
			castDuration = isChargeTimeFixed ? baseCastDuration : calculateChargeCastDuration();
			castSpeedForAnimationBoostAndChargeSkills = (float) castDuration / baseCastDuration;
		} else {
			castDuration = calculateCastDuration();
			castSpeedForAnimationBoostAndChargeSkills = 1 - effector.getGameStats().getStat(StatEnum.BOOST_CASTING_TIME, 1000).getBonus() / 1000f;
		}
	}

	private boolean updateChargeBaseCastDuration() {
		// cast/attack speed can affect charge time since 4.8 (https://aionpowerbook.com/powerbook/New_World_Update_-_Skill_Changes#Other_Changes)
		boolean isChargeTimeFixed = !isCastDurationAffectedByCastSpeed(); // fear and sleep charge skills are excluded, just like with regular casts
		SkillChargeCondition chargeCondition = skillTemplate.getSkillChargeCondition();
		if (chargeCondition != null) {
			int maxCastDuration = 0;
			ChargeSkillEntry skillCharge = DataManager.SKILL_CHARGE_DATA.getChargedSkillEntry(chargeCondition.getValue());
			for (ChargedSkill chargedSkill : skillCharge.getSkills()) {
				if (!isChargeTimeFixed && !DataManager.SKILL_DATA.getSkillTemplate(chargedSkill.getId()).isCastDurationAffectedByCastSpeed())
					isChargeTimeFixed = true;
				maxCastDuration += chargedSkill.getTime();
			}
			baseCastDuration = maxCastDuration;
		}
		return isChargeTimeFixed;
	}

	private int calculateChargeCastDuration() {
		boolean isPhysicalClass = effector instanceof Player player
			&& (player.getPlayerClass().isPhysicalClass() || player.getPlayerClass() == PlayerClass.RIDER || player.getPlayerClass() == PlayerClass.GUNNER);
		int castDuration;
		if (isPhysicalClass) // TODO check if attack speed should also affect magical classes
			castDuration = (int) effector.getGameStats().getPositiveStat(StatEnum.ATTACK_SPEED, baseCastDuration);
		else
			castDuration = calculateMagicalCastDuration();
		return Math.max(castDuration, (int) (baseCastDuration * 0.3f)); // TODO check limit with retail
	}

	private int calculateCastDuration() {
		// ap & cash revival stones, or 2nd+ time of multicast-skill activation
		if (getSkillId() == 10802 || getMultiCastCount() > 0)
			return 0;
		if (skillTemplate.getType() != SkillType.MAGICAL || !isCastDurationAffectedByCastSpeed())
			return baseCastDuration;
		return calculateMagicalCastDuration();
	}

	private int calculateMagicalCastDuration() {
		boolean noBaseDurationCap = false;
		int castDuration = effector.getGameStats().getPositiveReverseStat(StatEnum.BOOST_CASTING_TIME, baseCastDuration);
		int boostValue = effector.getGameStats().getPositiveReverseStat(StatEnum.BOOST_CASTING_TIME_SKILL, baseCastDuration);
		switch (skillTemplate.getSubType()) {
			case SUMMON:
				boostValue = effector.getGameStats().getPositiveReverseStat(StatEnum.BOOST_CASTING_TIME_SUMMON, boostValue);
				if (effector.getEffectController().hasAbnormalEffect(3779))
					noBaseDurationCap = true;
				break;
			case SUMMONHOMING:
				boostValue = effector.getGameStats().getPositiveReverseStat(StatEnum.BOOST_CASTING_TIME_SUMMONHOMING, boostValue);
				if (effector.getEffectController().hasAbnormalEffect(3779))
					noBaseDurationCap = true;
				break;
			case SUMMONTRAP:
				int tempBoostVal = boostValue;
				boostValue = effector.getGameStats().getPositiveReverseStat(StatEnum.BOOST_CASTING_TIME_TRAP, boostValue);
				if (boostValue == 0 && castDuration < tempBoostVal) {
					boostValue = tempBoostVal - castDuration;
				}
				if (effector.getEffectController().hasAbnormalEffect(913))
					noBaseDurationCap = true;
				break;
			case HEAL:
				boostValue = effector.getGameStats().getPositiveReverseStat(StatEnum.BOOST_CASTING_TIME_HEAL, boostValue);
				break;
			case ATTACK:
				boostValue = effector.getGameStats().getPositiveReverseStat(StatEnum.BOOST_CASTING_TIME_ATTACK, boostValue);
				break;
		}
		castDuration -= baseCastDuration - boostValue;

		// 70% of base skill castDuration cap
		// No cast speed cap for skill Summoning Alacrity I(skillId: 1778) and Nimble Fingers I(skillId: 2386)
		if (!noBaseDurationCap) {
			int baseDurationCap = Math.round(baseCastDuration * 0.3f);
			if (castDuration < baseDurationCap) {
				castDuration = baseDurationCap;
			}
		}
		return Math.max(castDuration, 0);
	}

	protected void updateHitTime(boolean checkAnimation) {
		hitTime = clientHitTime;
		if (!checkAnimation || !(effector instanceof Player player) || skillMethod != SkillMethod.CAST && skillMethod != SkillMethod.ITEM)
			return;

		float animationTimeUntilFirstHit = DataManager.MOTION_DATA.calculateAnimationTimeUntilFirstHit(player, this);
		int toleranceMillis = 1;
		if (skillTemplate.getAmmoSpeed() != 0) {
			float distance = (float) PositionUtil.getDistance(player, firstTarget);
			if (player.getMoveController().isInMove() || firstTarget.getMoveController().isInMove()) // subtract the run distance until ammo is actually fired
				distance -= PositionUtil.calculateMaxCoveredDistance(player, Math.round(animationTimeUntilFirstHit));
			float distanceTolerance = getDistanceTolerance(player, firstTarget);
			float ammoTime = Math.max(0, distance / skillTemplate.getAmmoSpeed() * 1000);
			toleranceMillis += Math.max(0, (int) Math.ceil(distanceTolerance / skillTemplate.getAmmoSpeed() * 1000));
			animationTimeUntilFirstHit += ammoTime;
		}

		int motionDelay = skillTemplate.getMotion() == null ? 0 : skillTemplate.getMotion().getDelay();
		int serverHitTime = motionDelay + Math.round(animationTimeUntilFirstHit);
		if (serverHitTime > clientHitTime) {
			hitTime = serverHitTime;
			if (isSuspiciousClientHitTime(clientHitTime, serverHitTime, toleranceMillis, player)) {
				List<String> uncertainties = collectUncertaintyFactorsForHitTime(player, toleranceMillis);
				String uncertaintyFactors = uncertainties.isEmpty() ? "" : " Uncertainty factors: " + String.join(", ", uncertainties);
				AuditLogger.log(player,
					"modified hit time for skill %d (client < server: %d/%d).%s".formatted(getSkillId(), clientHitTime, serverHitTime, uncertaintyFactors));
			}
		}
	}

	private float getDistanceTolerance(Player player, Creature target) {
		long nowMillis = System.currentTimeMillis();
		// even when not yet moving on server side, the player can just have started to move before casting (CM_MOVE is sent after CM_CASTSPELL)
		long maxMovementMillis = player.getMoveController().isInMove() ? 1000 : 200;
		long movementMillis = Math.min(maxMovementMillis, nowMillis - player.getMoveController().getLastMoveUpdate());
		float distanceTolerance = PositionUtil.calculateMaxCoveredDistance(player, movementMillis);
		if (target.getMoveController().isInMove())
			distanceTolerance += PositionUtil.calculateMaxCoveredDistance(target, nowMillis - target.getMoveController().getLastMoveUpdate());
		return distanceTolerance;
	}

	private boolean isSuspiciousClientHitTime(int clientHitTime, int serverHitTime, int tolerance, Player player) {
		if (clientHitTime >= serverHitTime - tolerance)
			return false;
		if (clientHitTime == 0 && (itemTemplate != null || skillTemplate.getMotion() != null && skillTemplate.getMotion().isInstantSkill()))
			return false; // effects apply immediately (damage too, though visually delayed)
		if (clientHitTime == 0 && player.isInRobotMode() && (player.getLastSkill().isMultiCast() || DataManager.SKILL_CHARGE_DATA.isChargeSkill(player.getLastSkill())))
			return false; // AT sends no hitTime when casting a non-instant skill within the animation time of a previous multiCast or charge skill, like 2640
		return true;
	}

	private List<String> collectUncertaintyFactorsForHitTime(Player player, int toleranceMillis) {
		List<String> uncertainties = new ArrayList<>();
		if (allowAnimationBoostByCastSpeed() && !player.isHitTimeBoosted())
			uncertainties.add("cast speed");
		if (skillTemplate.getAmmoSpeed() != 0)
			uncertainties.add("movement (calculated tolerance: " + toleranceMillis + " ms)");
		if (clientHitTime == 0 && player.isInRobotMode()) // TODO remove once isSuspiciousClientHitTime() identifies all false positives 
			uncertainties.add("Aethertech being weird 🤷‍♂️ (previous skill: " + player.getLastSkill().getSkillId() + ")");
		return uncertainties;
	}

	private void startPenaltySkill() {
		int penaltySkill = skillTemplate.getPenaltySkillId();
		if (penaltySkill == 0)
			return;
		if (getSkillTemplate().shouldPenaltySkillSendMsg()) {
			PenaltySkill penaltySkill1 = SkillEngine.getInstance().getPenaltySkill(effector, penaltySkill, 1);
			if (penaltySkill1 != null) {
				penaltySkill1.useSkill();
			}
		} else {
			SkillEngine.getInstance().applyEffectDirectly(penaltySkill, firstTarget, effector);
		}
	}

	/**
	 * Start casting of skill
	 */
	private void startCast() {
		int targetObjId = firstTarget != null ? firstTarget.getObjectId() : 0;
		boolean needsCast = itemTemplate != null && itemTemplate.isCombatActivated();
		if (skillMethod == SkillMethod.CAST || needsCast) {
			switch (targetType) {
				case 0: // PlayerObjectId as Target
					PacketSendUtility.broadcastPacketAndReceive(effector,
						new SM_CASTSPELL(effector, skillTemplate.getSkillId(), skillLevel, targetType, targetObjId, castDuration,
							castSpeedForAnimationBoostAndChargeSkills, allowAnimationBoostByCastSpeed()));
					if (effector instanceof Npc) {
						ShoutEventHandler.onCast((NpcAI) effector.getAi(), firstTarget);
					}
					break;

				case 3: // Target not in sight?
					PacketSendUtility.broadcastPacketAndReceive(effector,
						new SM_CASTSPELL(effector, skillTemplate.getSkillId(), skillLevel, targetType, targetObjId, castDuration,
							castSpeedForAnimationBoostAndChargeSkills, allowAnimationBoostByCastSpeed()));
					break;

				case 1: // XYZ as Target
					PacketSendUtility.broadcastPacketAndReceive(effector,
						new SM_CASTSPELL(effector, skillTemplate.getSkillId(), skillLevel, targetType, x, y, z, castDuration,
							castSpeedForAnimationBoostAndChargeSkills, allowAnimationBoostByCastSpeed()));
					break;
			}
			if (effector instanceof Player player)
				player.setNextSkillUse(System.currentTimeMillis() + GSConfig.MIN_SKILL_CAST_INTERVAL_MILLIS);
		} else if (skillMethod == SkillMethod.ITEM && castDuration > 0) {
			PacketSendUtility.broadcastPacketAndReceive(effector, new SM_ITEM_USAGE_ANIMATION(effector.getObjectId(), firstTarget.getObjectId(),
				itemObjectId, itemTemplate.getTemplateId(), castDuration, 0, 0));
		}

		if (firstTarget != null && !firstTarget.equals(effector) && !skillTemplate.hasResurrectEffect() && (castDuration > 0)
			&& skillTemplate.getProperties().getFirstTarget() != FirstTargetAttribute.POINT
			&& skillTemplate.getProperties().getFirstTarget() != FirstTargetAttribute.ME) {
			if ((effector instanceof Npc && ((Npc) effector).isBoss())
				|| (skillTemplate.getProperties().getFirstTarget() == FirstTargetAttribute.TARGET && skillTemplate.getProperties().getEffectiveDist() > 0)) {
				return;
			}
			firstTargetDieObserver = new ActionObserver(ObserverType.DEATH) {

				@Override
				public void died(Creature creature) {
					getEffector().getController().cancelCurrentSkill(null, SM_SYSTEM_MESSAGE.STR_SKILL_TARGET_LOST());
				}
			};
			firstTarget.getObserveController().attach(firstTargetDieObserver);
		}

	}

	public void cancelCast() {
		if (isCancelled)
			return;
		isCancelled = true;
		removeObservers();
	}

	private void cancelCurrentSkillCast() {
		if (!isCancelled && equals(effector.getCastingSkill()))
			effector.getController().cancelCurrentSkill(null, null);
	}

	/**
	 * Apply effects and perform actions specified in skill template
	 */
	protected void endCast() {
		removeObservers();
		if (!effector.isCasting() || isCancelled)
			return;
		// check if target is out of skill range or other requirements are not met (anymore)
		Properties properties = skillTemplate.getProperties();
		if (properties != null && !properties.endCastValidate(this) || !validateEffectedList() || !preUsageCheck()) {
			effector.getController().cancelCurrentSkill(null); // calls effector.setCasting(null) and sends skill cancel packet
			return;
		}
		effector.setCasting(null);

		// try removing item, if its not possible return to prevent exploits
		if (effector instanceof Player && skillMethod == SkillMethod.ITEM) {
			Item item = ((Player) effector).getInventory().getItemByObjId(itemObjectId);
			if (item == null)
				return;
			if (item.getActivationCount() > 1) {
				item.setActivationCount(item.getActivationCount() - 1);
			} else {
				if (!((Player) effector).getInventory().decreaseByObjectId(item.getObjectId(), 1, ItemUpdateType.DEC_ITEM_USE))
					return;
			}
		}

		endCondCheck();

		// Perform necessary actions (use mp,dp items etc)
		Actions skillActions = skillTemplate.getActions();
		if (skillActions != null) {
			for (Action action : skillActions.getActions()) {
				if (!action.act(this))
					return;
			}
		}

		// Create effects and precalculate result
		int dashStatus = 0;
		int resistCount = 0;
		boolean blockedChain = false;
		boolean blockedStance = false;
		final List<Effect> effects = new ArrayList<>();
		if (skillTemplate.getEffects() != null) {
			for (Creature effected : effectedList) {
				// TODO: RI_CHARGEATTACK fix: effect is not applied twice, but its Dash effect inflicts (weapon) damage.
				// Seems like Offi is creating a new "Effect" for each DamageEffect. Last one contains relevant info about spell status etc.
				// Client displays the wrong chat output in these cases. (e.g. RI_CHARGEATTACK displays 2x the spellatkinstant damage)
				Effect effect = new Effect(this, effected);
				effect.initialize();
				if (effected instanceof Player) {
					if (effect.getEffectResult() == EffectResult.CONFLICT)
						blockedStance = true;
				}
				final int worldId = effector.getWorldId();
				final int instanceId = effector.getInstanceId();
				effect.setWorldPosition(worldId, instanceId, x, y, z);
				effects.add(effect);
				if (firstTarget == null || firstTarget.equals(effected))
					dashStatus = effect.getDashStatus().getId();
				if (effect.getAttackStatus() == AttackStatus.RESIST || effect.getAttackStatus() == AttackStatus.DODGE) {
					resistCount++;
				}
			}

			if (resistCount == effectedList.size()) {
				blockedChain = true;
				blockedPenaltySkill = true;
			}

			// exception for point point skills(example Ice Sheet)
			if (effectedList.isEmpty()) {
				if (this.isPointPointSkill()) {
					Effect effect = new Effect(this, null);
					effect.initialize();
					effect.setWorldPosition(effector.getWorldId(), effector.getInstanceId(), x, y, z);
					effects.add(effect);
				}
			}
		}

		boolean setCooldowns = true;
		if (effector instanceof Player playerEffector) {
			if (skillTemplate.isStance() && !blockedStance && skillMethod == SkillMethod.CAST)
				playerEffector.getController().startStance(skillTemplate.getSkillId());
			if (getMultiCastCount() > 0)
				setCooldowns = false;

			// Check Chain Skill Trigger Rate, only for chain skills and only for player
			if (chainCategory != null) {
				if (blockedChain)
					chainSuccess = false;
				else
					chainSuccess = Rnd.chance() < skillTemplate.getChainSkillProb() || CustomConfig.SKILL_CHAIN_DISABLE_TRIGGERRATE;

				if (chainSuccess)
					playerEffector.getChainSkills().updateChain(chainCategory, chainUsageDuration);
				else
					playerEffector.getChainSkills().resetChain();
			}

			QuestEngine.getInstance().onUseSkill(new QuestEnv(effector.getTarget(), (Player) effector, 0), skillTemplate.getSkillId());
		}

		if (setCooldowns)
			setCooldowns();

		// Use penalty skill (now 100% success)
		if (!blockedPenaltySkill)
			startPenaltySkill();

		if (isInstantSkill())
			applyEffect(effects);
		else
			ThreadPoolManager.getInstance().schedule(() -> applyEffect(effects), hitTime);

		if (skillMethod == SkillMethod.PENALTY || skillMethod == SkillMethod.CAST || skillMethod == SkillMethod.ITEM) {
			boolean sentCastSpellResultPacket = sendCastSpellEnd(dashStatus, effects);
			if (sentCastSpellResultPacket && skillMethod != SkillMethod.PENALTY && effector instanceof Player player) {
				// animation times must be calculated after applyEffect of instant skills in order to honor speed buffs from this skill
				AnimationTimes animation = DataManager.MOTION_DATA.calculateAnimationTimesAfterLastHit(player, this);
				long nowMillis = System.currentTimeMillis();
				if (animation != null && allowAnimationBoostByCastSpeed()) {
					int latencyToleranceMillis = 50; // animation starts after client receives SM_CASTSPELL_RESULT, so add a few milliseconds
					player.setHitTimeBoost(nowMillis + animation.fullDurationMillis() + latencyToleranceMillis, getCastSpeedForAnimationBoostAndChargeSkills());
				} else {
					player.setHitTimeBoost(0, 0);
				}
				if (animation != null) // Math.max because nextSkillUse set from startCast() must not be undercut
					player.setNextSkillUse(Math.max(player.getNextSkillUse(), nowMillis + animation.lastHitMillis()));
			}
		}

		if (skillTemplate.isDeityAvatar() && effector instanceof Player player) {
			AbyssService.announceAbyssSkillUsage(player, skillTemplate.getL10n());
		}

		effector.getAi().onEndUseSkill(skillTemplate, skillLevel);
		if (effector instanceof Npc npc) {
			NpcSkillEntry lastSkill = npc.getGameStats().getLastSkill();
			if (lastSkill != null)
				lastSkill.fireOnEndCastEvents(npc);

			SkillAttackManager.afterUseSkill((NpcAI) npc.getAi());
		}

		if (skillMethod == SkillMethod.CAST) {
			effector.getObserveController().notifyEndSkillCastObservers(this);
		}
		effector.getWorldMapInstance().getInstanceHandler().onEndCastSkill(this);
	}

	private void removeObservers() {
		if (firstTargetDieObserver != null)
			firstTarget.getObserveController().removeObserver(firstTargetDieObserver);
		effector.getObserveController().removeObserver(moveListener);
	}

	private void addResistedEffectHateAndNotifyFriends(List<Effect> effects) {
		if (effects == null || effects.isEmpty()) {
			return;
		}
		effects.stream()
			.filter(
				effect -> effect.getTauntHate() >= 0 && (effect.getAttackStatus() == AttackStatus.RESIST || effect.getAttackStatus() == AttackStatus.DODGE))
			.forEach(effect -> {
				effect.getEffected().getAggroList().addHate(effector, 1);
				effect.getEffected().getKnownList()
					.forEachNpc(object -> object.getAi().onCreatureEvent(AIEventType.CREATURE_NEEDS_SUPPORT, effect.getEffected()));
			});
	}

	private void applyEffect(List<Effect> effects) {
		// Apply effects to effected objects
		effects.forEach(Effect::applyEffect);

		addResistedEffectHateAndNotifyFriends(effects);
	}

	private boolean sendCastSpellEnd(int dashStatus, List<Effect> effects) {
		boolean sentCastSpellPacket = false;
		if (itemTemplate != null && !itemTemplate.isCombatActivated()) {
			PacketSendUtility.broadcastPacketAndReceive(effector,
				new SM_ITEM_USAGE_ANIMATION(effector.getObjectId(), firstTarget.getObjectId(), itemObjectId, itemTemplate.getTemplateId(), 0, 1, 0));
		} else {
			AIEventType et = skillTemplate.getSubType() == SkillSubType.ATTACK ? AIEventType.CREATURE_NEEDS_HELP : null;
			switch (targetType) {
				case 0: // PlayerObjectId as Target
				case 3: // Target not in sight?
					PacketSendUtility.broadcastPacketAndReceive(effector, new SM_CASTSPELL_RESULT(this, effects, hitTime, chainSuccess, dashStatus), et);
					sentCastSpellPacket = true;
					break;
				case 1: // XYZ as Target
					PacketSendUtility.broadcastPacketAndReceive(effector,
						new SM_CASTSPELL_RESULT(this, effects, hitTime, chainSuccess, dashStatus, targetType), et);
					sentCastSpellPacket = true;
					break;
			}
		}
		if (skillMethod == SkillMethod.ITEM && effector instanceof Player player)
			PacketSendUtility.sendPacket(player, SM_SYSTEM_MESSAGE.STR_USE_ITEM(getItemTemplate().getL10n()));
		return sentCastSpellPacket;
	}

	/**
	 * Check all conditions before starting cast
	 */
	private boolean preCastCheck() {
		Conditions skillConditions = skillTemplate.getStartconditions();
		return skillConditions == null || skillConditions.validate(this);
	}

	/**
	 * Check all conditions before using skill
	 */
	private boolean preUsageCheck() {
		Conditions skillConditions = skillTemplate.getUseconditions();
		return skillConditions == null || skillConditions.validate(this);
	}

	/**
	 * Check all conditions after using skill
	 */
	private boolean endCondCheck() {
		Conditions skillConditions = skillTemplate.getEndConditions();
		return skillConditions == null || skillConditions.validate(this);
	}

	/**
	 * @param value
	 *          is the changeMpConsumptionValue to set
	 */
	public void setBoostSkillCost(int value) {
		boostSkillCost = value;
	}

	/**
	 * @return the changeMpConsumptionValue
	 */
	public int getBoostSkillCost() {
		return boostSkillCost;
	}

	/**
	 * @return the effectedList
	 */
	public List<Creature> getEffectedList() {
		return effectedList;
	}

	/**
	 * @return the effector
	 */
	public Creature getEffector() {
		return effector;
	}

	/**
	 * @return the skillLevel
	 */
	public int getSkillLevel() {
		return skillLevel;
	}

	/**
	 * @return the skillId
	 */
	public int getSkillId() {
		return skillTemplate.getSkillId();
	}

	/**
	 * @return the conditionChangeListener
	 */
	public StartMovingListener getMoveListener() {
		return moveListener;
	}

	/**
	 * @return the skillTemplate
	 */
	public SkillTemplate getSkillTemplate() {
		return skillTemplate;
	}

	/**
	 * @return the firstTarget
	 */
	public Creature getFirstTarget() {
		return firstTarget;
	}

	/**
	 * @param firstTarget
	 *          the firstTarget to set
	 */
	public void setFirstTarget(Creature firstTarget) {
		this.firstTarget = firstTarget;
	}

	/**
	 * @return true or false
	 */
	public boolean isPassive() {
		return skillTemplate.getActivationAttribute() == ActivationAttribute.PASSIVE;
	}

	/**
	 * @return the firstTargetRangeCheck
	 */
	public boolean isFirstTargetRangeCheck() {
		return firstTargetRangeCheck;
	}

	public FirstTargetAttribute getFirstTargetAttribute() {
		return skillTemplate.getProperties() == null ? null : skillTemplate.getProperties().getFirstTarget();
	}

	public TargetRangeAttribute getTargetRangeAttribute() {
		return skillTemplate.getProperties() == null ? null : skillTemplate.getProperties().getTargetType();
	}

	/**
	 * @return true if the present skill is a non-targeted, non-point AOE skill
	 */
	public boolean isNonTargetAOE() {
		return getFirstTargetAttribute() == FirstTargetAttribute.ME && getTargetRangeAttribute() == TargetRangeAttribute.AREA;
	}

	/**
	 * @return true if the present skill is a targeted AOE skill
	 */
	private boolean isTargetAOE() {
		return getFirstTargetAttribute() == FirstTargetAttribute.TARGET && getTargetRangeAttribute() == TargetRangeAttribute.AREA;
	}

	/**
	 * @return true if the present skill is a self buff includes items (such as scroll buffs)
	 */
	public boolean isSelfBuff() {
		return getFirstTargetAttribute() == FirstTargetAttribute.ME && getTargetRangeAttribute() == TargetRangeAttribute.ONLYONE
			&& skillTemplate.getSubType() == SkillSubType.BUFF && !skillTemplate.isDeityAvatar();
	}

	/**
	 * @return true if the present skill has self as first target
	 */
	public boolean isFirstTargetSelf() {
		return getFirstTargetAttribute() == FirstTargetAttribute.ME;
	}

	/**
	 * @return true if the present skill is a Point skill
	 */
	public boolean isPointSkill() {
		return getFirstTargetAttribute() == FirstTargetAttribute.POINT;
	}

	/**
	 * @param firstTargetRangeCheck
	 *          the firstTargetRangeCheck to set
	 */
	public void setFirstTargetRangeCheck(boolean firstTargetRangeCheck) {
		this.firstTargetRangeCheck = firstTargetRangeCheck;
	}

	public ItemTemplate getItemTemplate() {
		return itemTemplate;
	}

	public void setItemObjectId(int id) {
		this.itemObjectId = id;
	}

	public int getItemObjectId() {
		return itemObjectId;
	}

	public void setTargetType(int targetType, float x, float y, float z) {
		this.targetType = targetType;
		this.x = x;
		this.y = y;
		this.z = z;
	}

	/**
	 * Calculated position after skill
	 */
	public void setTargetPosition(float x, float y, float z, byte h) {
		this.x = x;
		this.y = y;
		this.z = z;
		this.h = h;
	}

	public float getX() {
		return x;
	}

	public float getY() {
		return y;
	}

	public float getZ() {
		return z;
	}

	public final byte getH() {
		return h;
	}

	protected void setCastStartTime(long castStartTime) {
		this.castStartTime = castStartTime;
	}

	public void setClientHitTime(int time) {
		this.clientHitTime = time;
	}

	public int getHitTime() {
		return hitTime;
	}

	protected void setCastSpeedForAnimationBoostAndChargeSkills(float castSpeedForAnimationBoostAndChargeSkills) {
		this.castSpeedForAnimationBoostAndChargeSkills = castSpeedForAnimationBoostAndChargeSkills;
	}

	public float getCastSpeedForAnimationBoostAndChargeSkills() {
		return castSpeedForAnimationBoostAndChargeSkills;
	}

	/**
	 * The game client allows to boost the animation time of a skill via cast speed:<br>
	 * - only half of the {@link #castSpeedForAnimationBoostAndChargeSkills cast speed boost} is considered and only if this boost exceeds the attack speed boost
	 * - animation time considers the current cast speed boost<br>
	 * - hit time considers cast speed boost of the previously cast skill
	 * - hit time will only be boosted if the current skill if cast before the animation of the previous skill finishes<br>
	 */
	public boolean allowAnimationBoostByCastSpeed() {
		return isMagical();
	}

	private boolean isCastDurationAffectedByCastSpeed() {
		return skillMethod == SkillMethod.CAST && skillTemplate.isCastDurationAffectedByCastSpeed();
	}

	public void setChainCategory(String chainCategory) {
		this.chainCategory = chainCategory;
	}

	public void setChainUsageDuration(int duration) {
		this.chainUsageDuration = duration;
	}

	public SkillMethod getSkillMethod() {
		return this.skillMethod;
	}

	private boolean isPointPointSkill() {
		return this.getSkillTemplate().getProperties().getFirstTarget() == FirstTargetAttribute.POINT
			&& this.getSkillTemplate().getProperties().getTargetType() == TargetRangeAttribute.POINT;

	}

	public int getMultiCastCount() {
		return skillTemplate.isMultiCast() && effector instanceof Player p ? p.getChainSkills().getCurrentChainCount(chainCategory) : 0;
	}

	public long getCastStartTime() {
		return castStartTime;
	}

	public boolean isInstantSkill() {
		return hitTime == 0 || skillTemplate.getMotion() != null && skillTemplate.getMotion().isInstantSkill();
	}

	public int getHate() {
		return hate;
	}

	public void setHate(int hate) {
		this.hate = hate;
	}

	private boolean isMagical() {
		return skillTemplate.getType() == SkillType.MAGICAL && skillTemplate.getSubType() != SkillSubType.NONE;
	}
}

📎 첨부파일

댓글 작성 권한이 없습니다.
🏆 포인트 랭킹 TOP 10
순위 닉네임 포인트
1 no_profile 타키야겐지쪽지보내기 자기소개 아이디로 검색 전체게시물 102,949
2 no_profile 동가리쪽지보내기 자기소개 아이디로 검색 전체게시물 63,733
3 no_profile 라프텔쪽지보내기 자기소개 아이디로 검색 전체게시물 51,771
4 no_profile 불멸의행복쪽지보내기 자기소개 아이디로 검색 전체게시물 36,923
5 서번트쪽지보내기 자기소개 아이디로 검색 전체게시물 35,011
6 no_profile 닥터스쪽지보내기 자기소개 아이디로 검색 전체게시물 29,470
7 no_profile 검은고양이쪽지보내기 자기소개 아이디로 검색 전체게시물 29,077
8 no_profile Revolution쪽지보내기 자기소개 아이디로 검색 전체게시물 28,199
9 no_profile 보거스쪽지보내기 자기소개 아이디로 검색 전체게시물 26,731
10 no_profile 호롤롤로쪽지보내기 자기소개 아이디로 검색 전체게시물 17,020
알림 0