Files
Server/src/engine/gameManager/NPCManager.java
T
MagicBotandClaude Opus 5.5 6b4e9815e0 Switch Item.template and MobLoot to CObjectTemplate
Item.template is now the cache template from CObjectTemplate.lookup
instead of the legacy DB ItemTemplate, so item.template.cacheTemplate.X
becomes item.template.X. MobLoot, LootManager, ItemManager.isTwoHanded/
isShield and CombatManager.getSwingAnimation take a CObjectTemplate.

No value changes: every getter already read the cache template through
the bridge, and the lookup resolves the same 7586 ids as
static_item_templates with the same item_type (offline parity p17.py,
0 differences).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-20 07:55:28 -04:00

567 lines
23 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.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);
}
}
}
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 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()));
}
}
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) {
ItemTemplate template = ItemTemplate.templates.get(templateID);
float rollingDuration;
if (template == null)
return 0;
if (template.cacheTemplate.getItemBaneRank() > 0)
return (long) (template.cacheTemplate.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;
}
}