Files
Server/src/engine/gameManager/NPCManager.java
T
MagicBotandClaude Opus 5.5 fb7e641d9c Grant NECROPETSTATS to necro pets in code
No client data grants NECROPETSTATS (no rune, summon power or item; no
reference in sb.exe), but NEC-018 Sepulchre's Bounty and DRU-066
Braialla's Judgement require it on the target, so the server adds it to
Grobones 12021 and Wight 12022 in applyMobbaseEffects.

Rank is the highest rank of the pet's own ladder (PET_NECRO /
PET_NECRO_HARDHIT) at or below its level, which is exactly what the 100
server-made static_npc_mobbase_effects rows held (same 50 rank/reqLvl
pairs). Tying it to that ladder is a guess at the official behaviour.
No value changes.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-23 15:15:32 -04:00

600 lines
24 KiB
Java

// • ▌ ▄ ·. ▄▄▄· ▄▄ • ▪ ▄▄· ▄▄▄▄· ▄▄▄· ▐▄▄▄ ▄▄▄ .
// ·██ ▐███▪▐█ ▀█ ▐█ ▀ ▪██ ▐█ ▌▪▐█ ▀█▪▐█ ▀█ •█▌ ▐█▐▌·
// ▐█ ▌▐▌▐█·▄█▀▀█ ▄█ ▀█▄▐█·██ ▄▄▐█▀▀█▄▄█▀▀█ ▐█▐ ▐▌▐▀▀▀
// ██ ██▌▐█▌▐█ ▪▐▌▐█▄▪▐█▐█▌▐███▌██▄▪▐█▐█ ▪▐▌██▐ █▌▐█▄▄▌
// ▀▀ █▪▀▀▀ ▀ ▀ ·▀▀▀▀ ▀▀▀·▀▀▀ ·▀▀▀▀ ▀ ▀ ▀▀ █▪ ▀▀▀
// Magicbane Emulator Project © 2013 - 2026
// https://www.magicbane.com
package engine.gameManager;
import engine.InterestManagement.WorldGrid;
import engine.loot.WorkOrder;
import engine.math.Quaternion;
import engine.math.Vector3f;
import engine.math.Vector3fImmutable;
import engine.mbEnums;
import engine.net.Dispatch;
import engine.net.client.ClientConnection;
import engine.net.client.msg.ErrorPopupMsg;
import engine.net.client.msg.PetMsg;
import engine.objects.*;
import engine.parsers.objects.CObjectTemplate;
import engine.powers.EffectsBase;
import engine.powers.RuneSkillAdjustEntry;
import engine.server.MBServerStatics;
import org.pmw.tinylog.Logger;
import java.util.ArrayList;
import java.util.EnumMap;
import java.util.HashMap;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ThreadLocalRandom;
import java.util.concurrent.TimeUnit;
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);
DispatchManager.dispatchMsgDispatch(dispatch, mbEnums.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 = mbEnums.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(mbEnums.ProtectionState.CONTRACT))
protectedBuildings.add(b);
if (b.getProtectionState().equals(mbEnums.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(mbEnums.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(mbEnums.GameObjectType.Mob) && ((Mob) abstractCharacter).behaviourType.equals(mbEnums.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 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(mbEnums.AIAgentType.GUARDWALLARCHER)) {
//apply rogue bonuses
attackRatingModifier += 0.5f;
defenseModifier += 0.5f;
damageModifier += 0.5f;
attackSpeedModifier -= 0.36f;
} else {
Integer contractID;
if (guard.agentType.equals(mbEnums.AIAgentType.GUARDMINION)) {
contractID = guard.guardCaptain.contract.getContractID();
} else {
contractID = guard.contract.getContractID();
}
if (mbEnums.MinionType.ContractToMinionMap.get(contractID) != null && mbEnums.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) {
float rankModifier = 1 + (guard.getRank() * 0.1f);
int primaryStat = 0;
if (guard.charItemManager.equipped.isEmpty()) {
guard.minDamageHandOne = (int) ((guard.mobBase.getDamageMin()) * rankModifier);
guard.maxDamageHandOne = (int) ((guard.mobBase.getDamageMax()) * rankModifier);
guard.speedHandOne = 30.0f;
} else {
if (guard.charItemManager.equipped.containsKey(mbEnums.EquipSlotType.RHELD)) {
//has main hand weapon
Item weapon = guard.charItemManager.equipped.get(mbEnums.EquipSlotType.RHELD);
if (engine.parsers.objects.ItemTemplate._items.get(weapon.templateID).item_primary_attr == mbEnums.AttributeType.Strength)
primaryStat = guard.getStatStrCurrent();
else
primaryStat = guard.getStatDexCurrent();
guard.minDamageHandOne = (int) ((guard.mobBase.getDamageMin() + engine.parsers.objects.ItemTemplate.getWeapon(weapon.templateID).weapon_damage.get(0).damage_min) * rankModifier) + primaryStat;
guard.maxDamageHandOne = (int) ((guard.mobBase.getDamageMax() + engine.parsers.objects.ItemTemplate.getWeapon(weapon.templateID).weapon_damage.get(0).damage_max) * rankModifier) + primaryStat;
guard.speedHandOne = engine.parsers.objects.ItemTemplate.getWeapon(weapon.templateID).weapon_wepspeed;
guard.rangeHandOne = engine.parsers.objects.ItemTemplate.getWeapon(weapon.templateID).weapon_max_range;
} else if (guard.charItemManager.equipped.containsKey(mbEnums.EquipSlotType.LHELD) && !ItemManager.isShield(guard.charItemManager.equipped.get(mbEnums.EquipSlotType.LHELD).template)) {
//has off hand weapon
Item weapon = guard.charItemManager.equipped.get(mbEnums.EquipSlotType.LHELD);
if (engine.parsers.objects.ItemTemplate._items.get(weapon.templateID).item_primary_attr == mbEnums.AttributeType.Strength)
primaryStat = guard.getStatStrCurrent();
else
primaryStat = guard.getStatDexCurrent();
guard.minDamageHandOne = (int) ((guard.mobBase.getDamageMin() + engine.parsers.objects.ItemTemplate.getWeapon(weapon.templateID).weapon_damage.get(0).damage_min) * rankModifier) + primaryStat;
guard.maxDamageHandOne = (int) ((guard.mobBase.getDamageMax() + engine.parsers.objects.ItemTemplate.getWeapon(weapon.templateID).weapon_damage.get(0).damage_max) * rankModifier) + primaryStat;
guard.speedHandOne = engine.parsers.objects.ItemTemplate.getWeapon(weapon.templateID).weapon_wepspeed;
guard.rangeHandOne = engine.parsers.objects.ItemTemplate.getWeapon(weapon.templateID).weapon_max_range;
} else {
primaryStat = guard.getStatStrCurrent();
guard.minDamageHandOne = (int) ((guard.mobBase.getDamageMin()) * rankModifier) + primaryStat;
guard.maxDamageHandOne = (int) ((guard.mobBase.getDamageMax()) * rankModifier) + primaryStat;
guard.speedHandOne = 30.0f;
guard.rangeHandOne = 3;
}
}
}
public static void setDefenseForGuard(Mob guard) {
int dexterity = guard.getStatDexCurrent();
if (dexterity < 1)
dexterity = 1;
int baseDef = guard.mobBase.getDefenseRating();
int armorDefense = 0;
for (Item equipped : guard.charItemManager.equipped.values())
if (equipped.template.getItemType().equals(mbEnums.ItemType.ARMOR) || ItemManager.isShield(equipped.template))
armorDefense += engine.parsers.objects.ItemTemplate._items.get(equipped.templateID).item_defense_rating;
guard.defenseRating = dexterity + baseDef + armorDefense;
}
public static void setAttackRatingForGuard(Mob guard) {
int strength = guard.getStatStrCurrent();
int baseAtr = guard.mobBase.getAttackRating();
if (guard.charItemManager.equipped.get(mbEnums.EquipSlotType.RHELD) != null)
guard.atrHandOne = baseAtr + (int) ((strength * 0.5f) + ((int) guard.charItemManager.equipped.get(mbEnums.EquipSlotType.RHELD).template.getItemSkillReq().values().toArray()[0] * 4) + ((int) guard.charItemManager.equipped.get(mbEnums.EquipSlotType.RHELD).template.getItemSkillReq().values().toArray()[0] * 3));
else if (guard.charItemManager.equipped.get(mbEnums.EquipSlotType.LHELD) != null && !ItemManager.isShield(guard.charItemManager.equipped.get(mbEnums.EquipSlotType.LHELD).template))
guard.atrHandTwo = baseAtr + (int) ((strength * 0.5f) + ((int) guard.charItemManager.equipped.get(mbEnums.EquipSlotType.LHELD).template.getItemSkillReq().values().toArray()[0] * 4) + ((int) guard.charItemManager.equipped.get(mbEnums.EquipSlotType.LHELD).template.getItemSkillReq().values().toArray()[0] * 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;
}
guard.setHealth(guard.healthMax);
}
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);
}
}
if (mob.isNecroPet())
applyNecroPetStats(mob);
}
// No client data grants NECROPETSTATS, yet NEC-018 and DRU-066 require it on the target; the server adds it to necro pets
private static void applyNecroPetStats(Mob mob) {
EffectsBase necroPetStats = PowersManager.getEffectByIDString("NECROPETSTATS");
String effectKey;
Effect currentEffect;
int rank = -1;
if (necroPetStats == null) {
Logger.error("NECROPETSTATS effect not loaded");
return;
}
// GUESS: rank follows the pet's own level ladder (PET_NECRO / PET_NECRO_HARDHIT, from reqLvl 9), as the old DB rows did
for (MobBaseEffects mbe : mob.mobBase.effectsList)
if (mbe.getReqLvl() > 0 && mbe.getReqLvl() <= (int) mob.level)
rank = Math.max(rank, mbe.getRank());
if (rank < 0)
return;
effectKey = Integer.toString(necroPetStats.getUUID());
currentEffect = mob.effects.get(effectKey);
if (currentEffect != null) {
if (currentEffect.getTrains() >= rank)
return;
currentEffect.cancelJob();
}
mob.addEffectNoTimer(effectKey, necroPetStats, rank, true);
}
public static void applyEquipmentResists(Mob mob) {
EnumMap<mbEnums.DamageType, Float> armorResists;
if (mob.charItemManager.equipped.isEmpty())
return;
for (Item equipped : mob.charItemManager.equipped.values()) {
if (equipped.template.getItemType().equals(mbEnums.ItemType.ARMOR)) {
armorResists = equipped.template.getCombatAttackResist();
mob.resists.setResist(mbEnums.DamageType.SLASHING, mob.resists.getResist(mbEnums.DamageType.SLASHING, 0) + armorResists.getOrDefault(mbEnums.DamageType.SLASHING, 0f));
mob.resists.setResist(mbEnums.DamageType.CRUSHING, mob.resists.getResist(mbEnums.DamageType.CRUSHING, 0) + armorResists.getOrDefault(mbEnums.DamageType.CRUSHING, 0f));
mob.resists.setResist(mbEnums.DamageType.PIERCING, mob.resists.getResist(mbEnums.DamageType.PIERCING, 0) + armorResists.getOrDefault(mbEnums.DamageType.PIERCING, 0f));
}
}
}
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.skill_type) == 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()));
}
}
public static void applyRunesForNPC(NPC npc) {
npc.runes = new ArrayList<>();
RuneBase shopkeeperBase = RuneBase.getRuneBase(252620);
CharacterRune shopkeeper = new CharacterRune(shopkeeperBase, npc.getObjectUUID());
npc.runes.add(shopkeeper);
if (NPCManager._runeSetMap.containsKey(npc.runeSetID)) {
for (int runeID : _runeSetMap.get(npc.runeSetID)) {
// Shopkeeper is already applied above and most rune sets repeat it
if (runeID == 252620)
continue;
RuneBase rb = RuneBase.getRuneBase(runeID);
if (rb != null) {
CharacterRune toApply = new CharacterRune(rb, npc.getObjectUUID());
npc.runes.add(toApply);
}
}
}
}
public static Boolean NPCVaultBankRangeCheck(PlayerCharacter pc, ClientConnection origin, String bankorvault) {
if (pc == null)
return false;
NPC npc = pc.getLastNPCDialog();
if (npc == null)
return false;
// System.out.println(npc.getContract().getName());
// last npc must be either a banker or vault keeper
if (bankorvault.equals("vault")) {
if (npc.getContract().getContractID() != 861)
return false;
} else
// assuming banker
if (!npc.getContract().getName().equals("Bursar"))
return false;
if (pc.getLoc().distanceSquared2D(npc.getLoc()) > MBServerStatics.NPC_TALK_RANGE * MBServerStatics.NPC_TALK_RANGE) {
ErrorPopupMsg.sendErrorPopup(pc, 14);
return false;
} else
return true;
}
public static ArrayList<Item> getAllCookingForVendor(NPC npc) {
ArrayList<Item> itemList = new ArrayList<>();
ConcurrentHashMap.KeySetView<WorkOrder, Boolean> vendorWorkOrders = ForgeManager.vendorWorkOrderLookup.get(npc);
if (vendorWorkOrders == null)
return itemList;
for (WorkOrder workOrder : vendorWorkOrders)
itemList.addAll(workOrder.cooking);
return itemList;
}
public static long calcRollingDuration(NPC vendor, int templateID) {
CObjectTemplate template = CObjectTemplate.lookup(templateID);
float rollingDuration;
if (template == null)
return 0;
if (template.getItemBaneRank() > 0)
return (long) (template.getItemBaneRank() * 60 * 60 * 3 * 1000 * Float.parseFloat(ConfigManager.MB_PRODUCTION_RATE.getValue()));
if (vendor.building == null)
return 600;
rollingDuration = vendor.getBuilding().getRank() * -5L + 40;
rollingDuration = TimeUnit.MINUTES.toMillis((long) rollingDuration);
rollingDuration *= Float.parseFloat(ConfigManager.MB_PRODUCTION_RATE.getValue());
return (long) rollingDuration;
}
}