package com.aionemu.gameserver.controllers;
import java.util.Collection;
import java.util.Set;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
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.poll.AIQuestion;
import com.aionemu.gameserver.controllers.attack.AggroInfo;
import com.aionemu.gameserver.controllers.attack.AggroList;
import com.aionemu.gameserver.controllers.attack.AttackStatus;
import com.aionemu.gameserver.custom.pvpmap.PvpMapHandler;
import com.aionemu.gameserver.dataholders.DataManager;
import com.aionemu.gameserver.instance.handlers.InstanceHandler;
import com.aionemu.gameserver.model.animations.ObjectDeleteAnimation;
import com.aionemu.gameserver.model.drop.DropItem;
import com.aionemu.gameserver.model.gameobjects.*;
import com.aionemu.gameserver.model.gameobjects.player.Player;
import com.aionemu.gameserver.model.gameobjects.player.Rates;
import com.aionemu.gameserver.model.gameobjects.state.CreatureState;
import com.aionemu.gameserver.model.team.TemporaryPlayerTeam;
import com.aionemu.gameserver.model.team.common.service.PlayerTeamDistributionService;
import com.aionemu.gameserver.network.aion.serverpackets.SM_ATTACK_STATUS.LOG;
import com.aionemu.gameserver.network.aion.serverpackets.SM_ATTACK_STATUS.TYPE;
import com.aionemu.gameserver.network.aion.serverpackets.SM_PET;
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.DialogService;
import com.aionemu.gameserver.services.RespawnService;
import com.aionemu.gameserver.services.SiegeService;
import com.aionemu.gameserver.services.abyss.AbyssPointsService;
import com.aionemu.gameserver.services.drop.DropRegistrationService;
import com.aionemu.gameserver.services.drop.DropService;
import com.aionemu.gameserver.services.event.EventService;
import com.aionemu.gameserver.skillengine.model.Effect;
import com.aionemu.gameserver.skillengine.model.HopType;
import com.aionemu.gameserver.skillengine.model.SkillTemplate;
import com.aionemu.gameserver.taskmanager.tasks.MoveTaskManager;
import com.aionemu.gameserver.utils.PacketSendUtility;
import com.aionemu.gameserver.utils.PositionUtil;
import com.aionemu.gameserver.utils.stats.StatFunctions;
import com.aionemu.gameserver.world.World;
import com.aionemu.gameserver.world.geo.GeoService;
import com.aionemu.gameserver.world.zone.ZoneInstance;
/**
* This class is for controlling Npc's
*
* @author -Nemesiss-, ATracer (2009-09-29), Sarynth, Wakizashi
*/
public class NpcController extends CreatureController<Npc> {
private static final Logger log = LoggerFactory.getLogger(NpcController.class);
@Override
public void see(VisibleObject object) {
super.see(object);
if (object instanceof Creature creature) {
getOwner().getAi().onCreatureEvent(AIEventType.CREATURE_SEE, creature);
}
}
@Override
public void notSee(VisibleObject object, ObjectDeleteAnimation animation) {
if (object instanceof Creature creature) {
getOwner().getAi().onCreatureEvent(AIEventType.CREATURE_NOT_SEE, creature);
}
super.notSee(object, animation);
}
@Override
public void onBeforeSpawn() {
super.onBeforeSpawn();
Npc owner = getOwner();
// set state from npc templates
if (owner.getObjectTemplate().getState() > 0)
owner.setState(owner.getObjectTemplate().getState());
else
owner.setState(CreatureState.WALK_MODE);
owner.getLifeStats().setCurrentHpPercent(100);
owner.getAi().onGeneralEvent(AIEventType.BEFORE_SPAWNED);
if (owner.getSpawn().getState() > 0) {
owner.setState(owner.getSpawn().getState());
} else if (owner.getSpawn().canFly()) {
owner.setState(CreatureState.FLYING);
}
}
@Override
public void onAfterSpawn() {
super.onAfterSpawn();
getOwner().getAi().onGeneralEvent(AIEventType.SPAWNED);
}
@Override
public void onDespawn() {
Npc owner = getOwner();
cancelCurrentSkill(null);
owner.getEffectController().removeAllEffects();
DropService.getInstance().unregisterDrop(owner);
owner.getPosition().getWorldMapInstance().getInstanceHandler().onDespawn(owner);
owner.getAi().onGeneralEvent(AIEventType.DESPAWNED);
getOwner().getObserveController().clear();
super.onDespawn();
}
@Override
public void onDie(Creature lastAttacker) {
Npc owner = getOwner();
if (owner.getSpawn().hasPool())
owner.getSpawn().setUse(owner.getInstanceId(), false);
boolean allowDecay = true;
boolean allowRespawn = true;
boolean shouldLoot = true;
try {
allowDecay = owner.getAi().ask(AIQuestion.ALLOW_DECAY);
allowRespawn = owner.getAi().ask(AIQuestion.ALLOW_RESPAWN);
shouldLoot = owner.getAi().ask(AIQuestion.REWARD_LOOT);
if (owner.getAi().ask(AIQuestion.REWARD_AP_XP_DP_LOOT))
doReward();
owner.getPosition().getWorldMapInstance().getInstanceHandler().onDie(owner);
owner.getAi().onGeneralEvent(AIEventType.DIED);
} catch (Exception e) {
log.error("onDie() exception for " + owner + ":", e);
}
super.onDie(lastAttacker);
if (allowRespawn && SiegeService.getInstance().isRespawnAllowed(owner))
RespawnService.scheduleRespawn(getOwner());
if (allowDecay) {
if (shouldLoot)
petLoot(owner);
RespawnService.scheduleDecayTask(owner);
if (getOwner().getSpawn() != null && getOwner().getSpawn().getStaticId() > 0) {
GeoService.getInstance().despawnPlaceableObject(getOwner().getWorldId(), getOwner().getInstanceId(), getOwner().getSpawn().getStaticId());
}
} else { // instant despawn (no decay time = no loot)
delete();
}
}
private void petLoot(Npc owner) {
Pet lootingPet = findPetForLooting(owner);
if (lootingPet != null && PositionUtil.isInRange(owner, lootingPet.getMaster(), 28, false)) {
int npcObjId = owner.getObjectId();
Set<DropItem> drops = DropRegistrationService.getInstance().getCurrentDropMap().get(npcObjId);
if (drops != null && !drops.isEmpty()) {
PacketSendUtility.sendPacket(lootingPet.getMaster(), new SM_PET(PetSpecialFunction.AUTOLOOT, true, npcObjId));
for (DropItem dropItem : drops.toArray(new DropItem[drops.size()])) // array copy since the drops get removed on retrieval
DropService.getInstance().requestDropItem(lootingPet.getMaster(), npcObjId, dropItem.getIndex(), true);
PacketSendUtility.sendPacket(lootingPet.getMaster(), new SM_PET(PetSpecialFunction.AUTOLOOT, false, npcObjId));
}
}
}
private Pet findPetForLooting(Npc npc) {
DropNpc dropNpc = DropRegistrationService.getInstance().getDropRegistrationMap().get(npc.getObjectId());
if (dropNpc == null) // npc didn't drop anything
return null;
if (dropNpc.getAllowedLooters().size() != 1) // auto looting is not available in FFA loot mode
return null;
Player player = World.getInstance().getPlayer(dropNpc.getAllowedLooters().iterator().next());
if (player == null) // looter got disconnected
return null;
Pet pet = player.getPet();
return pet != null && pet.getCommonData().isLooting() ? pet : null;
}
@SuppressWarnings("lossy-conversions")
@Override
public void doReward() {
super.doReward();
AggroList list = getOwner().getAggroList();
Collection<AggroInfo> finalList = list.getFinalDamageList(true);
AionObject winner = list.getMostDamage();
if (winner == null) {
return;
}
float totalDmg = 0;
for (AggroInfo info : finalList) {
totalDmg += info.getDamage();
}
if (totalDmg <= 0) {
log.warn("WARN total damage to " + getOwner().getName() + " is " + totalDmg + " reward process was skiped!");
return;
}
InstanceHandler instanceHandler = getOwner().getPosition().getWorldMapInstance().getInstanceHandler();
float apMultiplier = instanceHandler.getApMultiplier();
for (AggroInfo info : finalList) {
AionObject attacker = info.getAttacker();
if (attacker instanceof Npc) // don't reward npcs or summons
continue;
float percentage = info.getDamage() / totalDmg;
if (attacker instanceof TemporaryPlayerTeam<?> tmpPlayerTeam) {
PlayerTeamDistributionService.doReward(tmpPlayerTeam, percentage, getOwner(), winner);
} else if (attacker instanceof Player player && player.isInGroup()) {
PlayerTeamDistributionService.doReward(player.getPlayerGroup(), percentage, getOwner(), winner);
} else if (attacker instanceof Player player) {
if (!player.isDead()) {
// Reward init
long rewardXp = StatFunctions.calculateExperienceReward(player.getLevel(), getOwner());
int rewardDp = StatFunctions.calculateDPReward(player, getOwner());
float rewardAp = 1;
// Dmg percent correction
rewardXp *= percentage;
rewardDp *= percentage;
rewardAp *= percentage;
rewardAp *= apMultiplier;
boolean shouldNotifyQuestEngine = !(instanceHandler instanceof PvpMapHandler); // do not include pvp map
if (shouldNotifyQuestEngine)
QuestEngine.getInstance().onKill(new QuestEnv(getOwner(), player, 0));
EventService.getInstance().onPveKill(player, getOwner());
player.getCommonData().addExp(rewardXp, Rates.XP_HUNTING, getOwner().getObjectTemplate().getL10n());
player.getCommonData().addDp(rewardDp);
if (getOwner().getAi().ask(AIQuestion.REWARD_AP)) {
int calculatedAp = StatFunctions.calculatePvEApGained(player, getOwner());
rewardAp *= calculatedAp;
if (rewardAp >= 1) {
AbyssPointsService.addAp(player, getOwner(), (int) rewardAp);
}
}
}
if (attacker.equals(winner) && getOwner().getAi().ask(AIQuestion.REWARD_LOOT))
DropRegistrationService.getInstance().registerDrop(getOwner(), player, player.getLevel(), null);
}
}
}
@Override
public void onDialogRequest(Player player) {
// notify npc dialog request observer
if (!getOwner().getObjectTemplate().canInteract())
return;
if (!PositionUtil.isInTalkRange(player, getOwner())) {
if (getOwner().getObjectTemplate().isDialogNpc())
PacketSendUtility.sendPacket(player, SM_SYSTEM_MESSAGE.STR_DIALOG_TOO_FAR_TO_TALK());
else
PacketSendUtility.sendPacket(player, SM_SYSTEM_MESSAGE.STR_WAREHOUSE_TOO_FAR_FROM_NPC());
return;
}
getOwner().getAi().onCreatureEvent(AIEventType.DIALOG_START, player);
}
@Override
public void onDialogSelect(int dialogActionId, int prevDialogId, Player player, int questId, int extendedRewardIndex) {
if (!PositionUtil.isInTalkRange(player, getOwner())) {
// TODO remove this call and return unconditionally once logging has clarified wtf it is trying to solve
if (QuestEngine.getInstance().onDialog(new QuestEnv(getOwner(), player, questId, dialogActionId)))
log.warn(player + " talked to " + getOwner() + " out of range, but QuestEngine.onDialog returned true");
else
return;
}
if (!getOwner().getAi().onDialogSelect(player, dialogActionId, questId, extendedRewardIndex)) {
DialogService.onDialogSelect(dialogActionId, player, getOwner(), questId, extendedRewardIndex);
}
}
@Override
public void onAddHate(Creature attacker, boolean isNewInAggroList) {
if (isNewInAggroList && attacker instanceof Player) {
if (((Player) attacker).isInTeam()) {
for (Player player : ((Player) attacker).getCurrentTeam().filterMembers(m -> PositionUtil.isInRange(getOwner(), m, 50)))
QuestEngine.getInstance().onAddAggroList(new QuestEnv(getOwner(), player, 0));
} else {
QuestEngine.getInstance().onAddAggroList(new QuestEnv(getOwner(), (Player) attacker, 0));
}
}
super.onAddHate(attacker, isNewInAggroList);
}
@Override
public void onAttack(Creature attacker, Effect effect, TYPE type, int damage, boolean notifyAttack, LOG logId, AttackStatus attackStatus,
HopType hopType) {
if (getOwner().isDead())
return;
final Creature actingCreature;
// summon should gain its own aggro (except if despawned, for example because of a damage over time effect)
if (attacker instanceof Summon && attacker.isSpawned())
actingCreature = attacker;
else
actingCreature = attacker.getActingCreature();
super.onAttack(actingCreature, effect, type, damage, notifyAttack, logId, attackStatus, hopType);
Npc npc = getOwner();
ShoutEventHandler.onEnemyAttack((NpcAI) npc.getAi(), attacker);
if (actingCreature instanceof Player)
QuestEngine.getInstance().onAttack(new QuestEnv(npc, (Player) actingCreature, 0));
}
@Override
public void onStartMove() {
super.onStartMove();
MoveTaskManager.getInstance().addCreature(getOwner());
}
@Override
public void onStopMove() {
super.onStopMove();
MoveTaskManager.getInstance().removeCreature(getOwner());
}
@Override
public void onEnterZone(ZoneInstance zoneInstance) {
if (zoneInstance.getAreaTemplate().getZoneName() == null) {
log.error("No name found for a Zone in the map " + zoneInstance.getAreaTemplate().getWorldId());
}
}
@Override
public boolean useSkill(int skillId, int skillLevel) {
SkillTemplate skillTemplate = DataManager.SKILL_DATA.getSkillTemplate(skillId);
if (!getOwner().isSkillDisabled(skillTemplate)) {
getOwner().getGameStats().renewLastSkillTime();
return super.useSkill(skillId, skillLevel);
}
return false;
}
public void loseAggro(boolean restoreHp) {
getOwner().setTarget(null);
getOwner().getAggroList().clear();
if (restoreHp)
getOwner().getLifeStats().triggerRestoreTask();
}
}