|
|
|
package engine.gameManager;
|
|
|
|
|
|
|
|
import engine.Enum;
|
|
|
|
import engine.InterestManagement.WorldGrid;
|
|
|
|
import engine.math.Quaternion;
|
|
|
|
import engine.math.Vector3f;
|
|
|
|
import engine.math.Vector3fImmutable;
|
|
|
|
import engine.mobileAI.MobAI;
|
|
|
|
import engine.net.Dispatch;
|
|
|
|
import engine.net.DispatchMessage;
|
|
|
|
import engine.net.client.msg.PetMsg;
|
|
|
|
import engine.objects.*;
|
|
|
|
import engine.powers.EffectsBase;
|
|
|
|
import engine.powers.PowersBase;
|
|
|
|
import engine.powers.RuneSkillAdjustEntry;
|
|
|
|
import org.pmw.tinylog.Logger;
|
|
|
|
|
|
|
|
import javax.smartcardio.ATR;
|
|
|
|
import java.util.ArrayList;
|
|
|
|
import java.util.HashMap;
|
|
|
|
import java.util.concurrent.ThreadLocalRandom;
|
|
|
|
|
|
|
|
import static engine.math.FastMath.acos;
|
|
|
|
|
|
|
|
public enum NPCManager {
|
|
|
|
|
|
|
|
NPC_MANAGER;
|
|
|
|
public static HashMap<Integer, ArrayList<Integer>> _runeSetMap = new HashMap<>();
|
|
|
|
|
|
|
|
public static void dismissNecroPet(Mob necroPet, boolean updateOwner) {
|
|
|
|
|
|
|
|
necroPet.setCombatTarget(null);
|
|
|
|
necroPet.hasLoot = false;
|
|
|
|
|
|
|
|
if (necroPet.parentZone != null)
|
|
|
|
necroPet.parentZone.zoneMobSet.remove(necroPet);
|
|
|
|
|
|
|
|
try {
|
|
|
|
necroPet.clearEffects();
|
|
|
|
} catch (Exception e) {
|
|
|
|
Logger.error(e.getMessage());
|
|
|
|
}
|
|
|
|
necroPet.playerAgroMap.clear();
|
|
|
|
WorldGrid.RemoveWorldObject(necroPet);
|
|
|
|
|
|
|
|
DbManager.removeFromCache(necroPet);
|
|
|
|
|
|
|
|
|
|
|
|
PlayerCharacter petOwner = (PlayerCharacter) necroPet.guardCaptain;
|
|
|
|
|
|
|
|
if (petOwner != null) {
|
|
|
|
|
|
|
|
necroPet.guardCaptain = null;
|
|
|
|
petOwner.setPet(null);
|
|
|
|
|
|
|
|
if (updateOwner == false)
|
|
|
|
return;
|
|
|
|
|
|
|
|
PetMsg petMsg = new PetMsg(5, null);
|
|
|
|
Dispatch dispatch = Dispatch.borrow(petOwner, petMsg);
|
|
|
|
DispatchMessage.dispatchMsgDispatch(dispatch, Enum.DispatchChannel.PRIMARY);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
public static void auditNecroPets(PlayerCharacter player) {
|
|
|
|
int removeIndex = 0;
|
|
|
|
while (player.necroPets.size() >= 10) {
|
|
|
|
|
|
|
|
|
|
|
|
if (removeIndex == player.necroPets.size())
|
|
|
|
break;
|
|
|
|
|
|
|
|
Mob necroPet = player.necroPets.get(removeIndex);
|
|
|
|
|
|
|
|
if (necroPet == null) {
|
|
|
|
removeIndex++;
|
|
|
|
continue;
|
|
|
|
}
|
|
|
|
dismissNecroPet(necroPet, true);
|
|
|
|
player.necroPets.remove(necroPet);
|
|
|
|
removeIndex++;
|
|
|
|
|
|
|
|
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
public static void resetNecroPets(PlayerCharacter player) {
|
|
|
|
|
|
|
|
for (Mob necroPet : player.necroPets)
|
|
|
|
if (necroPet.isPet())
|
|
|
|
necroPet.agentType = Enum.AIAgentType.MOBILE;
|
|
|
|
}
|
|
|
|
|
|
|
|
public static void spawnNecroPet(PlayerCharacter playerCharacter, Mob mob) {
|
|
|
|
|
|
|
|
if (mob == null)
|
|
|
|
return;
|
|
|
|
|
|
|
|
if (mob.getMobBaseID() != 12021 && mob.getMobBaseID() != 12022)
|
|
|
|
return;
|
|
|
|
|
|
|
|
auditNecroPets(playerCharacter);
|
|
|
|
resetNecroPets(playerCharacter);
|
|
|
|
|
|
|
|
playerCharacter.necroPets.add(mob);
|
|
|
|
}
|
|
|
|
|
|
|
|
public static void dismissNecroPets(PlayerCharacter playerCharacter) {
|
|
|
|
|
|
|
|
|
|
|
|
if (playerCharacter.necroPets.isEmpty())
|
|
|
|
return;
|
|
|
|
|
|
|
|
for (Mob necroPet : playerCharacter.necroPets) {
|
|
|
|
|
|
|
|
try {
|
|
|
|
dismissNecroPet(necroPet, true);
|
|
|
|
} catch (Exception e) {
|
|
|
|
Logger.error(e);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
playerCharacter.necroPets.clear();
|
|
|
|
}
|
|
|
|
|
|
|
|
public static void loadAllPirateNames() {
|
|
|
|
|
|
|
|
DbManager.NPCQueries.LOAD_PIRATE_NAMES();
|
|
|
|
}
|
|
|
|
|
|
|
|
public static String getPirateName(int mobBaseID) {
|
|
|
|
|
|
|
|
ArrayList<String> nameList = null;
|
|
|
|
|
|
|
|
// If we cannot find name for this mobbase then
|
|
|
|
// fallback to human male
|
|
|
|
|
|
|
|
if (NPC._pirateNames.containsKey(mobBaseID))
|
|
|
|
nameList = NPC._pirateNames.get(mobBaseID);
|
|
|
|
else
|
|
|
|
nameList = NPC._pirateNames.get(2111);
|
|
|
|
|
|
|
|
if (nameList == null) {
|
|
|
|
Logger.error("Null name list for 2111!");
|
|
|
|
}
|
|
|
|
|
|
|
|
return nameList.get(ThreadLocalRandom.current().nextInt(nameList.size()));
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
public static ArrayList<Building> getProtectedBuildings(NPC npc) {
|
|
|
|
|
|
|
|
ArrayList<Building> protectedBuildings = new ArrayList<>();
|
|
|
|
|
|
|
|
if (npc.building == null)
|
|
|
|
return protectedBuildings;
|
|
|
|
|
|
|
|
if (npc.building.getCity() == null)
|
|
|
|
return protectedBuildings;
|
|
|
|
|
|
|
|
for (Building b : npc.building.getCity().getParent().zoneBuildingSet) {
|
|
|
|
|
|
|
|
if (b.getBlueprint() == null)
|
|
|
|
continue;
|
|
|
|
|
|
|
|
if (b.getProtectionState().equals(Enum.ProtectionState.CONTRACT))
|
|
|
|
protectedBuildings.add(b);
|
|
|
|
|
|
|
|
if (b.getProtectionState().equals(Enum.ProtectionState.PENDING))
|
|
|
|
protectedBuildings.add(b);
|
|
|
|
}
|
|
|
|
|
|
|
|
return protectedBuildings;
|
|
|
|
}
|
|
|
|
|
|
|
|
public static int slotCharacterInBuilding(AbstractCharacter abstractCharacter) {
|
|
|
|
|
|
|
|
int buildingSlot;
|
|
|
|
|
|
|
|
if (abstractCharacter.building == null)
|
|
|
|
return -1;
|
|
|
|
|
|
|
|
// Get next available slot for this NPC and use it
|
|
|
|
// to add the NPC to the building's hireling list
|
|
|
|
// Account for R8's having slots reversed.
|
|
|
|
|
|
|
|
if (abstractCharacter.building.getBlueprint() != null && abstractCharacter.building.getBlueprint().getBuildingGroup().equals(Enum.BuildingGroup.TOL) && abstractCharacter.building.getRank() == 8)
|
|
|
|
buildingSlot = BuildingManager.getLastAvailableSlot(abstractCharacter.building);
|
|
|
|
else
|
|
|
|
buildingSlot = BuildingManager.getAvailableSlot(abstractCharacter.building);
|
|
|
|
|
|
|
|
// Override slot for siege engines
|
|
|
|
|
|
|
|
if (abstractCharacter.getObjectType().equals(Enum.GameObjectType.Mob) && ((Mob) abstractCharacter).behaviourType.equals(Enum.MobBehaviourType.SiegeEngine)) {
|
|
|
|
Mob siegeMobile = (Mob) abstractCharacter;
|
|
|
|
buildingSlot = siegeMobile.guardCaptain.minions.size() + 2;
|
|
|
|
}
|
|
|
|
|
|
|
|
if (buildingSlot == -1)
|
|
|
|
Logger.error("No available slot for NPC: " + abstractCharacter.getObjectUUID());
|
|
|
|
|
|
|
|
// Pets are regular mobiles not hirelings (Siege engines)
|
|
|
|
if (abstractCharacter.contract != null)
|
|
|
|
abstractCharacter.building.getHirelings().put(abstractCharacter, buildingSlot);
|
|
|
|
|
|
|
|
// Override bind and location for this npc derived
|
|
|
|
// from BuildingManager slot location data.
|
|
|
|
|
|
|
|
Vector3fImmutable slotLocation = BuildingManager.getSlotLocation(abstractCharacter.building, buildingSlot).getLocation();
|
|
|
|
|
|
|
|
abstractCharacter.bindLoc = abstractCharacter.building.getLoc().add(slotLocation);
|
|
|
|
|
|
|
|
// Rotate slot position by the building rotation
|
|
|
|
|
|
|
|
abstractCharacter.bindLoc = Vector3fImmutable.rotateAroundPoint(abstractCharacter.building.getLoc(), abstractCharacter.bindLoc, abstractCharacter.building.getBounds().getQuaternion().angleY);
|
|
|
|
|
|
|
|
abstractCharacter.loc = new Vector3fImmutable(abstractCharacter.bindLoc);
|
|
|
|
|
|
|
|
// Rotate NPC rotation by the building's rotation
|
|
|
|
|
|
|
|
Quaternion slotRotation = new Quaternion().fromAngles(0, acos(abstractCharacter.getRot().y) * 2, 0);
|
|
|
|
slotRotation = slotRotation.mult(abstractCharacter.building.getBounds().getQuaternion());
|
|
|
|
abstractCharacter.setRot(new Vector3f(0, slotRotation.y, 0));
|
|
|
|
|
|
|
|
// Configure region and floor/level for this NPC
|
|
|
|
|
|
|
|
abstractCharacter.region = BuildingManager.GetRegion(abstractCharacter.building, abstractCharacter.bindLoc.x, abstractCharacter.bindLoc.y, abstractCharacter.bindLoc.z);
|
|
|
|
|
|
|
|
return buildingSlot;
|
|
|
|
}
|
|
|
|
|
|
|
|
public static int getMaxMinions(Mob guardCaptain) {
|
|
|
|
|
|
|
|
int maxSlots;
|
|
|
|
|
|
|
|
switch (guardCaptain.getRank()) {
|
|
|
|
case 3:
|
|
|
|
maxSlots = 2;
|
|
|
|
break;
|
|
|
|
case 4:
|
|
|
|
case 5:
|
|
|
|
maxSlots = 3;
|
|
|
|
break;
|
|
|
|
case 6:
|
|
|
|
maxSlots = 4;
|
|
|
|
break;
|
|
|
|
case 7:
|
|
|
|
maxSlots = 5;
|
|
|
|
break;
|
|
|
|
case 1:
|
|
|
|
case 2:
|
|
|
|
default:
|
|
|
|
maxSlots = 1;
|
|
|
|
|
|
|
|
}
|
|
|
|
return maxSlots;
|
|
|
|
}
|
|
|
|
|
|
|
|
public static void AssignPatrolPoints(Mob mob) {
|
|
|
|
mob.patrolPoints = new ArrayList<>();
|
|
|
|
|
|
|
|
for (int i = 0; i < 5; ++i) {
|
|
|
|
float patrolRadius = mob.getSpawnRadius();
|
|
|
|
|
|
|
|
if (patrolRadius > 256)
|
|
|
|
patrolRadius = 256;
|
|
|
|
|
|
|
|
if (patrolRadius < 60)
|
|
|
|
patrolRadius = 60;
|
|
|
|
|
|
|
|
Vector3fImmutable newPatrolPoint = Vector3fImmutable.getRandomPointInCircle(mob.getBindLoc(), patrolRadius);
|
|
|
|
mob.patrolPoints.add(newPatrolPoint);
|
|
|
|
|
|
|
|
if (i == 1) {
|
|
|
|
mob.setLoc(newPatrolPoint);
|
|
|
|
mob.endLoc = newPatrolPoint;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
public static void applyGuardStanceModifiers(Mob guard){
|
|
|
|
float damageModifier = 1;
|
|
|
|
float attackRatingModifier = 1;
|
|
|
|
float defenseModifier = 1;
|
|
|
|
float attackSpeedModifier = 1;
|
|
|
|
float powerDamageModifier = 1;
|
|
|
|
//handle stance modifiers for guard mob
|
|
|
|
if(guard.agentType.equals(Enum.AIAgentType.GUARDWALLARCHER)){
|
|
|
|
//apply rogue bonuses
|
|
|
|
attackRatingModifier += 0.5f;
|
|
|
|
defenseModifier += 0.5f;
|
|
|
|
}else {
|
|
|
|
Integer contractID;
|
|
|
|
if (guard.agentType.equals(Enum.AIAgentType.GUARDMINION)) {
|
|
|
|
contractID = guard.guardCaptain.contract.getContractID();
|
|
|
|
} else{
|
|
|
|
contractID = guard.contract.getContractID();
|
|
|
|
}
|
|
|
|
if (Enum.MinionType.ContractToMinionMap.get(contractID) != null && Enum.MinionType.ContractToMinionMap.get(contractID).isMage()){
|
|
|
|
//apply mage offensive Stance
|
|
|
|
powerDamageModifier += 0.5f;
|
|
|
|
} else{
|
|
|
|
//apply fighter offensive stance
|
|
|
|
damageModifier += 0.5f;
|
|
|
|
attackSpeedModifier -= 0.36f;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
guard.minDamageHandOne *= damageModifier;
|
|
|
|
guard.minDamageHandTwo *= damageModifier;
|
|
|
|
guard.maxDamageHandOne *= damageModifier;
|
|
|
|
guard.maxDamageHandTwo *= damageModifier;
|
|
|
|
guard.atrHandOne *= attackRatingModifier;
|
|
|
|
guard.atrHandTwo *= attackRatingModifier;
|
|
|
|
guard.defenseRating *= defenseModifier;
|
|
|
|
guard.speedHandOne *= attackSpeedModifier;
|
|
|
|
guard.speedHandTwo *= attackSpeedModifier;
|
|
|
|
|
|
|
|
//TODO figure out how to apply +50% powerdamage to mage guards
|
|
|
|
}
|
|
|
|
public static void setDamageAndSpeedForGuard(Mob guard){
|
|
|
|
|
|
|
|
|
|
|
|
if(guard.equip == null) {
|
|
|
|
guard.minDamageHandOne = (int)(guard.mobBase.getDamageMin());
|
|
|
|
guard.maxDamageHandOne = (int)(guard.mobBase.getDamageMax());
|
|
|
|
guard.speedHandOne = 30.0f;
|
|
|
|
}else{
|
|
|
|
if(guard.equip.containsKey(1)){
|
|
|
|
//has main hand weapon
|
|
|
|
ItemBase weapon = guard.equip.get(1).getItemBase();
|
|
|
|
guard.minDamageHandOne = (int)(guard.mobBase.getDamageMin() + weapon.getMinDamage());
|
|
|
|
guard.maxDamageHandOne = (int)(guard.mobBase.getDamageMax() + weapon.getMaxDamage());
|
|
|
|
guard.speedHandOne = weapon.getSpeed();
|
|
|
|
} else if(guard.equip.containsKey(2) && !guard.equip.get(2).getItemBase().isShield()){
|
|
|
|
//has off hand weapon
|
|
|
|
ItemBase weapon = guard.equip.get(2).getItemBase();
|
|
|
|
guard.minDamageHandTwo = (int)(guard.mobBase.getDamageMin() + weapon.getMinDamage());
|
|
|
|
guard.maxDamageHandTwo = (int)(guard.mobBase.getDamageMax() + weapon.getMaxDamage());
|
|
|
|
guard.speedHandTwo = weapon.getSpeed();
|
|
|
|
} else {
|
|
|
|
guard.minDamageHandOne = (int)(guard.mobBase.getDamageMin());
|
|
|
|
guard.maxDamageHandOne = (int)(guard.mobBase.getDamageMax());
|
|
|
|
guard.speedHandOne = 30.0f;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
public static void setDefenseForGuard(Mob guard){
|
|
|
|
int dexterity = guard.getStatDexCurrent();
|
|
|
|
if(dexterity < 1)
|
|
|
|
dexterity = 1;
|
|
|
|
int baseDef = guard.mobBase.getDefenseRating();
|
|
|
|
int armorDefense = 0;
|
|
|
|
for(MobEquipment equipped : guard.equip.values())
|
|
|
|
if(equipped.getItemBase().isArmor() || equipped.getItemBase().isShield())
|
|
|
|
armorDefense += equipped.getItemBase().getDefense();
|
|
|
|
guard.defenseRating = dexterity + baseDef + armorDefense;
|
|
|
|
}
|
|
|
|
|
|
|
|
public static void setAttackRatingForGuard(Mob guard) {
|
|
|
|
int strength = guard.getStatStrCurrent();
|
|
|
|
int baseAtr = guard.mobBase.getAttackRating();
|
|
|
|
if (guard.equip.get(1) != null)
|
|
|
|
guard.atrHandOne = baseAtr + (int) ((strength * 0.5f) + (guard.equip.get(1).getItemBase().getPercentRequired() * 4) + (guard.equip.get(1).getItemBase().getPercentRequired() * 3));
|
|
|
|
else if (guard.equip.get(2) != null && !guard.equip.get(2).getItemBase().isShield())
|
|
|
|
guard.atrHandTwo = baseAtr + (int) ((strength * 0.5f) + (guard.equip.get(2).getItemBase().getPercentRequired() * 4) + (guard.equip.get(2).getItemBase().getPercentRequired() * 3));
|
|
|
|
else
|
|
|
|
guard.atrHandOne = baseAtr;
|
|
|
|
}
|
|
|
|
|
|
|
|
public static void setMaxHealthForGuard(Mob guard){
|
|
|
|
//values derived fom reading memory address for health on client when selecting player guards
|
|
|
|
switch(guard.getRank()){
|
|
|
|
default:
|
|
|
|
guard.healthMax = 750; //rank 1
|
|
|
|
break;
|
|
|
|
case 2:
|
|
|
|
guard.healthMax = 2082;
|
|
|
|
break;
|
|
|
|
case 3:
|
|
|
|
guard.healthMax = 2740;
|
|
|
|
break;
|
|
|
|
case 4:
|
|
|
|
guard.healthMax = 3414;
|
|
|
|
break;
|
|
|
|
case 5:
|
|
|
|
guard.healthMax = 4080;
|
|
|
|
break;
|
|
|
|
case 6:
|
|
|
|
guard.healthMax = 4746;
|
|
|
|
break;
|
|
|
|
case 7:
|
|
|
|
guard.healthMax = 5412;
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
public static void applyMobbaseEffects(Mob mob) {
|
|
|
|
EffectsBase effectsBase;
|
|
|
|
for (MobBaseEffects mbe : mob.mobBase.effectsList) {
|
|
|
|
|
|
|
|
effectsBase = PowersManager.getEffectByToken(mbe.getToken());
|
|
|
|
|
|
|
|
if (effectsBase == null) {
|
|
|
|
Logger.info("Mob: " + mob.getObjectUUID() + " EffectsBase Null for Token " + mbe.getToken());
|
|
|
|
continue;
|
|
|
|
}
|
|
|
|
|
|
|
|
//check to upgrade effects if needed.
|
|
|
|
if (mob.effects.containsKey(Integer.toString(effectsBase.getUUID()))) {
|
|
|
|
|
|
|
|
if (mbe.getReqLvl() > (int) mob.level)
|
|
|
|
continue;
|
|
|
|
|
|
|
|
Effect eff = mob.effects.get(Integer.toString(effectsBase.getUUID()));
|
|
|
|
|
|
|
|
if (eff == null)
|
|
|
|
continue;
|
|
|
|
|
|
|
|
//Current effect is a higher rank, dont apply.
|
|
|
|
if (eff.getTrains() > mbe.getRank())
|
|
|
|
continue;
|
|
|
|
|
|
|
|
//new effect is of a higher rank. remove old effect and apply new one.
|
|
|
|
eff.cancelJob();
|
|
|
|
mob.addEffectNoTimer(Integer.toString(effectsBase.getUUID()), effectsBase, mbe.getRank(), true);
|
|
|
|
} else {
|
|
|
|
|
|
|
|
if (mbe.getReqLvl() > (int) mob.level)
|
|
|
|
continue;
|
|
|
|
|
|
|
|
mob.addEffectNoTimer(Integer.toString(effectsBase.getUUID()), effectsBase, mbe.getRank(), true);
|
|
|
|
}
|
|
|
|
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
public static void applyEquipmentResists(Mob mob){
|
|
|
|
if(mob.equip != null){
|
|
|
|
for(MobEquipment equipped : mob.equip.values()){
|
|
|
|
ItemBase itemBase = equipped.getItemBase();
|
|
|
|
if(itemBase.isHeavyArmor() || itemBase.isLightArmor() || itemBase.isMediumArmor()){
|
|
|
|
mob.resists.setResist(Enum.DamageType.Crush, mob.resists.getResist(Enum.DamageType.Crush,0) + itemBase.getCrushResist());
|
|
|
|
mob.resists.setResist(Enum.DamageType.Slash, mob.resists.getResist(Enum.DamageType.Slash,0) + itemBase.getCrushResist());
|
|
|
|
mob.resists.setResist(Enum.DamageType.Pierce, mob.resists.getResist(Enum.DamageType.Pierce,0) + itemBase.getCrushResist());
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
public static void applyMobbaseSkill(Mob mob) {
|
|
|
|
SkillsBase baseSkill = DbManager.SkillsBaseQueries.GET_BASE_BY_TOKEN(mob.mobBase.getMobBaseStats().getBaseSkill());
|
|
|
|
if(baseSkill != null)
|
|
|
|
mob.getSkills().put(baseSkill.getName(),new CharacterSkill(baseSkill,mob,mob.mobBase.getMobBaseStats().getBaseSkillAmount()));
|
|
|
|
}
|
|
|
|
|
|
|
|
public static void applyRuneSkills(Mob mob, int runeID){
|
|
|
|
//load mob skill adjustments from mobbase rune
|
|
|
|
if(PowersManager._allRuneSkillAdjusts.containsKey(runeID))
|
|
|
|
for(RuneSkillAdjustEntry entry : PowersManager._allRuneSkillAdjusts.get(runeID)) {
|
|
|
|
if(SkillsBase.getFromCache(entry.skill_type) == null)
|
|
|
|
SkillsBase.putInCache(DbManager.SkillsBaseQueries.GET_BASE_BY_NAME(entry.skill_type));
|
|
|
|
SkillsBase skillBase = SkillsBase.getFromCache(entry.skill_type);
|
|
|
|
if(skillBase == null)
|
|
|
|
continue;
|
|
|
|
if (entry.level <= mob.level)
|
|
|
|
if (mob.skills.containsKey(entry.name) == false)
|
|
|
|
mob.skills.put(entry.skill_type, new CharacterSkill(skillBase, mob, entry.rank));
|
|
|
|
else
|
|
|
|
mob.skills.put(entry.skill_type, new CharacterSkill(skillBase, mob, entry.rank + mob.skills.get(entry.skill_type).getNumTrains()));
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|