/*
* Copyright (C) 2004-2015 L2J Server
*
* This file is part of L2J Server.
*
* L2J Server is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* L2J Server is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see .
*/
package com.l2jserver.gameserver.util;
import java.util.List;
import java.util.concurrent.CopyOnWriteArrayList;
import java.util.logging.Logger;
import com.l2jserver.Config;
import com.l2jserver.gameserver.ThreadPoolManager;
import com.l2jserver.gameserver.data.xml.impl.NpcData;
import com.l2jserver.gameserver.model.Location;
import com.l2jserver.gameserver.model.actor.L2Character;
import com.l2jserver.gameserver.model.actor.instance.L2MonsterInstance;
import com.l2jserver.gameserver.model.actor.templates.L2NpcTemplate;
import com.l2jserver.gameserver.model.holders.MinionHolder;
import com.l2jserver.util.Rnd;
/**
* @author luisantonioa, DS
*/
public class MinionList
{
private static final Logger _log = Logger.getLogger(MinionList.class.getName());
protected final L2MonsterInstance _master;
/** List containing the current spawned minions */
private final List _minionReferences = new CopyOnWriteArrayList<>();
/** List containing the cached deleted minions for reuse */
protected List _reusedMinionReferences = null;
public MinionList(L2MonsterInstance pMaster)
{
if (pMaster == null)
{
throw new NullPointerException("MinionList: master is null");
}
_master = pMaster;
}
/**
* @return list of the spawned (alive) minions.
*/
public List getSpawnedMinions()
{
return _minionReferences;
}
/**
* Manage the spawn of Minions.
*
* Actions :
*
* Get the Minion data of all Minions that must be spawn For each Minion type, spawn the amount of Minion needed
*
* @param minions
*/
public final void spawnMinions(final List minions)
{
if (_master.isAlikeDead())
{
return;
}
// List minions = _master.getTemplate().getParameters().getMinionList("Privates");
if (minions == null)
{
return;
}
int minionCount, minionId;
long minionsToSpawn;
for (MinionHolder minion : minions)
{
minionCount = minion.getCount();
minionId = minion.getId();
minionsToSpawn = minionCount - countSpawnedMinionsById(minionId);
if (minionsToSpawn > 0)
{
for (int i = 0; i < minionsToSpawn; i++)
{
spawnMinion(minionId);
}
}
}
// remove non-needed minions
deleteReusedMinions();
}
/**
* Delete all spawned minions and try to reuse them.
*/
public void deleteSpawnedMinions()
{
if (!_minionReferences.isEmpty())
{
for (L2MonsterInstance minion : _minionReferences)
{
if (minion != null)
{
minion.setLeader(null);
minion.deleteMe();
if (_reusedMinionReferences != null)
{
_reusedMinionReferences.add(minion);
}
}
}
_minionReferences.clear();
}
}
/**
* Delete all reused minions to prevent memory leaks.
*/
public void deleteReusedMinions()
{
if (_reusedMinionReferences != null)
{
_reusedMinionReferences.clear();
}
}
// hooks
/**
* Called on the master spawn Old minions (from previous spawn) are deleted. If master can respawn - enabled reuse of the killed minions.
*/
public void onMasterSpawn()
{
deleteSpawnedMinions();
// if master has spawn and can respawn - try to reuse minions
if ((_reusedMinionReferences == null) && (_master.getTemplate().getParameters().getSet().get("SummonPrivateRate") == null) && !_master.getTemplate().getParameters().getMinionList("Privates").isEmpty() && (_master.getSpawn() != null) && _master.getSpawn().isRespawnEnabled())
{
_reusedMinionReferences = new CopyOnWriteArrayList<>();
}
}
/**
* Called on the minion spawn and added them in the list of the spawned minions.
* @param minion
*/
public void onMinionSpawn(L2MonsterInstance minion)
{
_minionReferences.add(minion);
}
/**
* Called on the master death/delete.
* @param force if true - force delete of the spawned minions By default minions deleted only for raidbosses
*/
public void onMasterDie(boolean force)
{
if (_master.isRaid() || force)
{
deleteSpawnedMinions();
}
}
/**
* Called on the minion death/delete. Removed minion from the list of the spawned minions and reuse if possible.
* @param minion
* @param respawnTime (ms) enable respawning of this minion while master is alive. -1 - use default value: 0 (disable) for mobs and config value for raids.
*/
public void onMinionDie(L2MonsterInstance minion, int respawnTime)
{
minion.setLeader(null); // prevent memory leaks
_minionReferences.remove(minion);
if (_reusedMinionReferences != null)
{
_reusedMinionReferences.add(minion);
}
final int time = respawnTime < 0 ? _master.isRaid() ? (int) Config.RAID_MINION_RESPAWN_TIMER : 0 : respawnTime;
if ((time > 0) && !_master.isAlikeDead())
{
ThreadPoolManager.getInstance().scheduleGeneral(new MinionRespawnTask(minion), time);
}
}
/**
* Called if master/minion was attacked. Master and all free minions receive aggro against attacker.
* @param caller
* @param attacker
*/
public void onAssist(L2Character caller, L2Character attacker)
{
if (attacker == null)
{
return;
}
if (!_master.isAlikeDead() && !_master.isInCombat())
{
_master.addDamageHate(attacker, 0, 1);
}
final boolean callerIsMaster = caller == _master;
int aggro = callerIsMaster ? 10 : 1;
if (_master.isRaid())
{
aggro *= 10;
}
for (L2MonsterInstance minion : _minionReferences)
{
if ((minion != null) && !minion.isDead() && (callerIsMaster || !minion.isInCombat()))
{
minion.addDamageHate(attacker, 0, aggro);
}
}
}
/**
* Called from onTeleported() of the master Alive and able to move minions teleported to master.
*/
public void onMasterTeleported()
{
final int offset = 200;
final int minRadius = (int) _master.getCollisionRadius() + 30;
for (L2MonsterInstance minion : _minionReferences)
{
if ((minion != null) && !minion.isDead() && !minion.isMovementDisabled())
{
int newX = Rnd.get(minRadius * 2, offset * 2); // x
int newY = Rnd.get(newX, offset * 2); // distance
newY = (int) Math.sqrt((newY * newY) - (newX * newX)); // y
if (newX > (offset + minRadius))
{
newX = (_master.getX() + newX) - offset;
}
else
{
newX = (_master.getX() - newX) + minRadius;
}
if (newY > (offset + minRadius))
{
newY = (_master.getY() + newY) - offset;
}
else
{
newY = (_master.getY() - newY) + minRadius;
}
minion.teleToLocation(new Location(newX, newY, _master.getZ()));
}
}
}
private final void spawnMinion(int minionId)
{
if (minionId == 0)
{
return;
}
// searching in reused minions
if (_reusedMinionReferences != null)
{
final L2MonsterInstance minion = _reusedMinionReferences.stream().filter(m -> (m.getId() == minionId)).findFirst().orElse(null);
if (minion != null)
{
_reusedMinionReferences.remove(minion);
minion.refreshID();
initializeNpcInstance(_master, minion);
return;
}
}
// not found in cache
spawnMinion(_master, minionId);
}
private final class MinionRespawnTask implements Runnable
{
private final L2MonsterInstance _minion;
public MinionRespawnTask(L2MonsterInstance minion)
{
_minion = minion;
}
@Override
public void run()
{
if (!_master.isAlikeDead() && _master.isVisible())
{
// minion can be already spawned or deleted
if (!_minion.isVisible())
{
if (_reusedMinionReferences != null)
{
_reusedMinionReferences.remove(_minion);
}
_minion.refreshID();
initializeNpcInstance(_master, _minion);
}
}
}
}
/**
* Init a Minion and add it in the world as a visible object.
*
* Actions :
*
* Get the template of the Minion to spawn Create and Init the Minion and generate its Identifier Set the Minion HP, MP and Heading Set the Minion leader to this RaidBoss Init the position of the Minion and add it in the world as a visible object
*
* @param master L2MonsterInstance used as master for this minion
* @param minionId The L2NpcTemplate Identifier of the Minion to spawn
* @return
*/
public static final L2MonsterInstance spawnMinion(L2MonsterInstance master, int minionId)
{
// Get the template of the Minion to spawn
L2NpcTemplate minionTemplate = NpcData.getInstance().getTemplate(minionId);
if (minionTemplate == null)
{
return null;
}
return initializeNpcInstance(master, new L2MonsterInstance(minionTemplate));
}
protected static final L2MonsterInstance initializeNpcInstance(L2MonsterInstance master, L2MonsterInstance minion)
{
minion.stopAllEffects();
minion.setIsDead(false);
minion.setDecayed(false);
// Set the Minion HP, MP and Heading
minion.setCurrentHpMp(minion.getMaxHp(), minion.getMaxMp());
minion.setHeading(master.getHeading());
// Set the Minion leader to this RaidBoss
minion.setLeader(master);
// move monster to masters instance
minion.setInstanceId(master.getInstanceId());
// Init the position of the Minion and add it in the world as a visible object
final int offset = 200;
final int minRadius = (int) master.getCollisionRadius() + 30;
int newX = Rnd.get(minRadius * 2, offset * 2); // x
int newY = Rnd.get(newX, offset * 2); // distance
newY = (int) Math.sqrt((newY * newY) - (newX * newX)); // y
if (newX > (offset + minRadius))
{
newX = (master.getX() + newX) - offset;
}
else
{
newX = (master.getX() - newX) + minRadius;
}
if (newY > (offset + minRadius))
{
newY = (master.getY() + newY) - offset;
}
else
{
newY = (master.getY() - newY) + minRadius;
}
minion.spawnMe(newX, newY, master.getZ());
if (Config.DEBUG)
{
_log.info("Spawned minion template " + minion.getId() + " with objid: " + minion.getObjectId() + " to boss " + master.getObjectId() + " ,at: " + minion.getX() + " x, " + minion.getY() + " y, " + minion.getZ() + " z");
}
return minion;
}
// Statistics part
private final long countSpawnedMinionsById(int minionId)
{
return _minionReferences.stream().filter(npc -> npc.getId() == minionId).count();
}
public final int countSpawnedMinions()
{
return _minionReferences.size();
}
public final long lazyCountSpawnedMinionsGroups()
{
return _minionReferences.stream().map(L2MonsterInstance::getId).distinct().count();
}
}