Files
Server/src/engine/gameManager/NPCManager.java
T
MagicBotandClaude Opus 5.5 566870de79 Move the remaining legacy item template lookups to CObjectTemplate
Dev commands, BuildingManager, ChatManager, ForgeManager, NPCManager,
NPC, Warehouse, WorkOrder and ManageNPCMsg now call
CObjectTemplate.lookup instead of ItemTemplate.templates.get. Forge
production cost reads getItemResourceCosts(). Only the DB loader and
linkCacheTemplates still use the legacy class (removed in Task 1.8).

Removes the static_item_templates ParityCheck comparator. Final parity
(offline p17.py, 7586 rows): every id has a cache template, 0 item_type
differences, 0 item_resource_costs differences (84 ITEMs have costs).

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

568 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.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);
}
}
}
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) {
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;
}
}