Files
Server/src/engine/objects/Mob.java
T
MagicBotandClaude Opus 5.5 5c58586f89 Send mob stats under the right attribute hashes
The mob message paired Dex with the Spirit hash (0xACB82E33), Int with
the Dexterity hash (0xE07B3336) and Spi with the Intelligence hash
(0xFF665EC3); the inline comments were wrong. Verified with
Hasher.SBStringHash. Mob and NPC now use MBServerStatics.STAT_*_ID.
Value change: the client now gets a mob's Dex, Int and Spi in the right
slots. NPCs send 0 for all five, so no change there.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-24 11:40:06 -04:00

1510 lines
47 KiB
Java

// • ▌ ▄ ·. ▄▄▄· ▄▄ • ▪ ▄▄· ▄▄▄▄· ▄▄▄· ▐▄▄▄ ▄▄▄ .
// ·██ ▐███▪▐█ ▀█ ▐█ ▀ ▪██ ▐█ ▌▪▐█ ▀█▪▐█ ▀█ •█▌ ▐█▐▌·
// ▐█ ▌▐▌▐█·▄█▀▀█ ▄█ ▀█▄▐█·██ ▄▄▐█▀▀█▄▄█▀▀█ ▐█▐ ▐▌▐▀▀▀
// ██ ██▌▐█▌▐█ ▪▐▌▐█▄▪▐█▐█▌▐███▌██▄▪▐█▐█ ▪▐▌██▐ █▌▐█▄▄▌
// ▀▀ █▪▀▀▀ ▀ ▀ ·▀▀▀▀ ▀▀▀·▀▀▀ ·▀▀▀▀ ▀ ▀ ▀▀ █▪ ▀▀▀
// Magicbane Emulator Project © 2013 - 2026
// https://www.magicbane.com
package engine.objects;
import engine.InterestManagement.WorldGrid;
import engine.gameManager.*;
import engine.job.JobScheduler;
import engine.jobs.DeferredPowerJob;
import engine.jobs.UpgradeNPCJob;
import engine.math.Bounds;
import engine.math.Vector3fImmutable;
import engine.mbEnums;
import engine.mbEnums.*;
import engine.net.ByteBufferWriter;
import engine.net.Dispatch;
import engine.net.client.msg.PetMsg;
import engine.net.client.msg.PlaceAssetMsg;
import engine.server.MBServerStatics;
import org.jetbrains.annotations.NotNull;
import org.joda.time.DateTime;
import org.pmw.tinylog.Logger;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.util.EnumSet;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.Delayed;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.ReentrantReadWriteLock;
import static engine.net.client.msg.ErrorPopupMsg.sendErrorPopup;
import static java.lang.Math.toIntExact;
public class Mob extends AbstractIntelligenceAgent implements Delayed {
private static int staticID = 0;
//mob specific
public final ConcurrentHashMap<Integer, Float> playerAgroMap = new ConcurrentHashMap<>(); //key = Player value = hate value
public final ReentrantReadWriteLock lock = new ReentrantReadWriteLock();
public long nextCastTime = 0;
public long nextCallForHelp = 0;
public ReentrantReadWriteLock minionLock = new ReentrantReadWriteLock();
public boolean despawned = false;
public Vector3fImmutable destination = Vector3fImmutable.ZERO;
public MobBase mobBase;
public int spawnDelay;
public Zone parentZone;
public boolean hasLoot = false;
public long deathTime = 0;
public long respawnTime = 0;
public int equipmentSetID = 0;
public int runeSet = 0;
public int bootySet = 0;
public int loadID;
public float spawnRadius;
//used by static mobs
public int parentZoneUUID;
//TODO implement feared object system
public AbstractWorldObject fearedObject = null;
protected int dbID; //the database ID
private int currentID;
public long nextAttackTime = System.currentTimeMillis();
private int lastMobPowerToken = 0;
private DeferredPowerJob weaponPower;
private DateTime upgradeDateTime = null;
private boolean lootSync = false;
// New Mobile constructor. Fill in the blanks and then call
// PERSIST.
public Mob() {
super();
this.dbID = MBServerStatics.NO_DB_ROW_ASSIGNED_YET;
this.currentID = MBServerStatics.NO_DB_ROW_ASSIGNED_YET;
this.bindLoc = Vector3fImmutable.ZERO;
this.gridObjectType = GridObjectType.DYNAMIC;
this.agentType = AIAgentType.MOBILE;
}
/**
* ResultSet Constructor
*/
public Mob(ResultSet rs) throws SQLException {
super(rs);
float statLat;
float statAlt;
float statLon;
try {
this.dbID = rs.getInt(1);
this.loadID = rs.getInt("mob_mobbaseID");
this.gridObjectType = GridObjectType.DYNAMIC;
this.agentType = AIAgentType.MOBILE;
this.spawnRadius = rs.getFloat("mob_spawnRadius");
this.spawnDelay = rs.getInt("mob_spawnTime");
statLat = rs.getFloat("mob_spawnX");
statAlt = rs.getFloat("mob_spawnY");
statLon = rs.getFloat("mob_spawnZ");
this.bindLoc = new Vector3fImmutable(statLat, statAlt, statLon);
this.parentZoneUUID = rs.getInt("parent");
this.level = (short) rs.getInt("mob_level");
this.buildingUUID = rs.getInt("mob_buildingID");
this.contractUUID = rs.getInt("mob_contractID");
this.guildUUID = rs.getInt("mob_guildUID");
this.equipmentSetID = rs.getInt("equipmentSet");
java.util.Date sqlDateTime;
sqlDateTime = rs.getTimestamp("upgradeDate");
if (sqlDateTime != null)
upgradeDateTime = new DateTime(sqlDateTime);
else
upgradeDateTime = null;
// Submit upgrade job if NPC is currently set to rank.
if (this.upgradeDateTime != null)
Mob.submitUpgradeJob(this);
if (this.mobBase != null && this.spawnDelay == 0)
this.spawnDelay = this.mobBase.getSpawnTime();
this.runeSet = rs.getInt("runeSet");
this.bootySet = rs.getInt("bootySet");
this.notEnemy = mbEnums.fromLong(rs.getLong("notEnemy"), mbEnums.MonsterType.class);
this.enemy = mbEnums.fromLong(rs.getLong("enemy"), mbEnums.MonsterType.class);
this.firstName = rs.getString("mob_name");
if (rs.getString("fsm").length() > 1)
this.behaviourType = MobBehaviourType.valueOf(rs.getString("fsm"));
this.currentID = this.dbID;
} catch (Exception e) {
Logger.error(e + " " + this.dbID);
}
}
public static void serializeMobForClientMsgOtherPlayer(Mob mob, ByteBufferWriter writer) {
Mob.serializeForClientMsgOtherPlayer(mob, writer);
}
public static void serializeForClientMsgOtherPlayer(Mob mob, ByteBufferWriter writer) {
writer.putInt(0);
writer.putInt(0);
int tid = (mob.mobBase != null) ? mob.mobBase.getObjectUUID() : 0;
if (mob.isPet()) {
writer.putInt(2);
writer.putInt(3);
writer.putInt(0);
writer.putInt(2522);
writer.putInt(GameObjectType.NPCClassRune.ordinal());
writer.putInt(mob.currentID);
} else if (tid == 100570) { //kur'adar
writer.putInt(3);
Mob.serializeRune(mob, writer, 3, GameObjectType.NPCClassRuneTwo.ordinal(), 2518); //warrior class
serializeRune(mob, writer, 5, GameObjectType.NPCClassRuneThree.ordinal(), 252621); //guard rune
} else if (tid == 100962 || tid == 100965) { //Spydraxxx the Mighty, Denigo Tantric
writer.putInt(2);
serializeRune(mob, writer, 5, GameObjectType.NPCClassRuneTwo.ordinal(), 252621); //guard rune
} else if (mob.contract != null || mob.isPlayerGuard()) {
writer.putInt(3);
serializeRune(mob, writer, 3, GameObjectType.NPCClassRuneTwo.ordinal(), 2518); //warrior class
serializeRune(mob, writer, 5, GameObjectType.NPCClassRuneThree.ordinal(), 252621); //guard rune
} else
writer.putInt(1);
//Generate Race Rune
writer.putInt(1);
writer.putInt(0);
if (mob.mobBase != null)
writer.putInt(mob.mobBase.getObjectUUID());
else
writer.putInt(mob.loadID);
writer.putInt(mob.getObjectType().ordinal());
writer.putInt(mob.currentID);
//Send Stats
writer.putInt(5);
writer.putInt(MBServerStatics.STAT_STR_ID);
writer.putInt(mob.statStrCurrent);
writer.putInt(MBServerStatics.STAT_DEX_ID);
writer.putInt(mob.statDexCurrent);
writer.putInt(MBServerStatics.STAT_CON_ID);
writer.putInt(mob.statConCurrent);
writer.putInt(MBServerStatics.STAT_INT_ID);
writer.putInt(mob.statIntCurrent);
writer.putInt(MBServerStatics.STAT_SPI_ID);
writer.putInt(mob.statSpiCurrent);
writer.putString(mob.firstName);
writer.putString(mob.lastName);
writer.putInt(0);
writer.putInt(0);
writer.putInt(0);
writer.putInt(0);
writer.put((byte) 0);
writer.putInt(mob.getObjectType().ordinal());
writer.putInt(mob.currentID);
if (mob.mobBase != null) {
writer.putFloat(mob.mobBase.getScale().x);
writer.putFloat(mob.mobBase.getScale().y);
writer.putFloat(mob.mobBase.getScale().z);
} else {
writer.putFloat(1.0f);
writer.putFloat(1.0f);
writer.putFloat(1.0f);
}
writer.putVector3f(mob.getLoc());
//Rotation
float radians = (float) Math.acos(mob.getRot().y) * 2;
if (mob.building != null)
if (mob.building.getBounds() != null && mob.building.getBounds().getQuaternion() != null)
radians += (mob.building.getBounds().getQuaternion()).angleY;
writer.putFloat(radians);
//Inventory Stuff
writer.putInt(0);
// get a copy of the equipped items.
if (!mob.charItemManager.equipped.isEmpty()) {
writer.putInt(mob.charItemManager.equipped.size());
for (Item me : mob.charItemManager.equipped.values())
Item._serializeForClientMsg(me, writer);
} else
writer.putInt(0);
writer.putInt(mob.getRank());
writer.putInt(mob.getLevel());
writer.putInt(mob.getIsSittingAsInt()); //Standing
writer.putInt(mob.getIsWalkingAsInt()); //Walking
writer.putInt(mob.getIsCombatAsInt()); //Combat
writer.putInt(2); //Unknown
writer.putInt(1); //Unknown - Headlights?
writer.putInt(0);
if (mob.building != null && mob.region != null) {
writer.putInt(mob.building.getObjectType().ordinal());
writer.putInt(mob.building.getObjectUUID());
} else {
writer.putInt(0); //<-Building Object Type
writer.putInt(0); //<-Building Object ID
}
writer.put((byte) 0);
writer.put((byte) 0);
writer.put((byte) 0);
writer.putInt(0); // NPC menu options
if (mob.contract != null && mob.guardCaptain == null) {
writer.put((byte) 1);
writer.putLong(0);
writer.putLong(0);
if (mob.contract != null)
writer.putInt(mob.contract.getIconID());
else
writer.putInt(0); //npc icon ID
} else
writer.put((byte) 0);
if (mob.guardCaptain != null) {
writer.put((byte) 1);
writer.putInt(GameObjectType.PlayerCharacter.ordinal());
writer.putInt(131117009);
writer.putInt(mob.guardCaptain.getObjectType().ordinal());
writer.putInt(mob.guardCaptain.getObjectUUID());
writer.putInt(8);
} else
writer.put((byte) 0);
if (mob.isPet()) {
writer.put((byte) 1);
if (mob.guardCaptain != null) {
writer.putInt(mob.guardCaptain.getObjectType().ordinal());
writer.putInt(mob.guardCaptain.getObjectUUID());
} else {
writer.putInt(0); //ownerType
writer.putInt(0); //ownerID
}
} else
writer.put((byte) 0);
writer.putInt(0);
writer.putInt(0);
writer.putInt(0);
writer.putInt(0);
writer.putInt(0);
writer.putInt(0);
writer.putInt(0);
writer.putInt(0);
writer.putInt(0);
if (!mob.isAlive() && !mob.isPet() && !mob.isNecroPet() && !mob.behaviourType.equals(MobBehaviourType.SiegeEngine) && !mob.isPlayerGuard()) {
writer.putInt(0);
writer.putInt(0);
}
writer.put((byte) 0);
Guild._serializeForClientMsg(mob.getGuild(), writer);
if (mob.mobBase != null && mob.mobBase.getObjectUUID() == 100570) {
writer.putInt(2);
writer.putInt(0x00008A2E);
writer.putInt(0x1AB84003);
} else if (mob.behaviourType.equals(MobBehaviourType.SiegeEngine)) {
writer.putInt(1);
writer.putInt(74620179);
} else
writer.putInt(0);
writer.putInt(0); //0xB8400300
writer.putInt(0);
//TODO Guard
writer.put((byte) 0);
writer.putFloat(mob.healthMax);
writer.putFloat(mob.health.get());
//TODO Peace Zone
writer.put((byte) 1); //0=show tags, 1=don't
//DON't LOAD EFFECTS FOR DEAD MOBS.
if (!mob.isAlive())
writer.putInt(0);
else {
int indexPosition = writer.position();
writer.putInt(0); //placeholder for item cnt
int total = 0;
for (Effect eff : mob.getEffects().values()) {
if (eff.isStatic())
continue;
if (!eff.serializeForLoad(writer))
continue;
++total;
}
writer.putIntAt(total, indexPosition);
}
// Effects
writer.put((byte) 0);
}
private static void serializeRune(Mob mob, ByteBufferWriter writer, int type, int objectType, int runeID) {
writer.putInt(type);
writer.putInt(0);
writer.putInt(runeID);
writer.putInt(objectType);
writer.putInt(mob.currentID);
}
public static Mob createMob(int loadID, Vector3fImmutable spawn, Guild guild, Zone parent, Building building, Contract contract, String pirateName, int level, AIAgentType mobType) {
Mob mobile = new Mob();
mobile.dbID = MBServerStatics.NO_DB_ROW_ASSIGNED_YET;
mobile.agentType = mobType;
mobile.behaviourType = MobBehaviourType.None;
mobile.loadID = loadID;
mobile.level = (short) level;
if (guild == null || guild.isEmptyGuild())
mobile.guildUUID = 0;
else
mobile.guildUUID = guild.getObjectUUID();
mobile.parentZoneUUID = parent.getObjectUUID();
if (building == null)
mobile.buildingUUID = 0;
else
mobile.buildingUUID = building.getObjectUUID();
if (mobile.buildingUUID != 0)
mobile.bindLoc = Vector3fImmutable.ZERO;
else
mobile.bindLoc = ZoneManager.worldToLocal(spawn, parent);
;
mobile.firstName = pirateName;
if (contract == null)
mobile.contractUUID = 0;
else
mobile.contractUUID = contract.getContractID();
return mobile;
}
// MB_MINION_SPAWN_TIME (seconds) shortens guard/siege minion spawns for testing; 0 or blank keeps the normal timers.
private static int minionSpawnDelay(int defaultDelay) {
String overrideValue;
int overrideDelay;
overrideValue = ConfigManager.MB_MINION_SPAWN_TIME.getValue();
if (overrideValue == null || overrideValue.trim().isEmpty())
return defaultDelay;
try {
overrideDelay = Integer.parseInt(overrideValue.trim());
} catch (NumberFormatException e) {
Logger.warn("MB_MINION_SPAWN_TIME is not a number: " + overrideValue);
return defaultDelay;
}
if (overrideDelay <= 0)
return defaultDelay;
return overrideDelay;
}
public static synchronized Mob createGuardMinion(Mob guardCaptain, short level, String minionName) {
Mob minionMobile;
minionMobile = new Mob();
minionMobile.currentID = (--Mob.staticID);
minionMobile.level = level;
minionMobile.loadID = guardCaptain.loadID;
minionMobile.firstName = minionName;
minionMobile.equipmentSetID = guardCaptain.equipmentSetID;
minionMobile.buildingUUID = guardCaptain.building.getObjectUUID();
minionMobile.guildUUID = guardCaptain.guildUUID;
minionMobile.runeSet = guardCaptain.runeSet;
minionMobile.enemy = guardCaptain.enemy;
minionMobile.notEnemy = guardCaptain.notEnemy;
minionMobile.deathTime = System.currentTimeMillis();
minionMobile.guardCaptain = guardCaptain;
minionMobile.spawnDelay = minionSpawnDelay((int) (-2.500 * guardCaptain.building.getRank() + 22.5) * 60);
minionMobile.behaviourType = mbEnums.MobBehaviourType.GuardMinion;
minionMobile.agentType = AIAgentType.GUARDMINION;
minionMobile.guardedCity = guardCaptain.guardedCity;
minionMobile.patrolPoints = guardCaptain.building.patrolPoints;
minionMobile.parentZoneUUID = guardCaptain.parentZoneUUID;
minionMobile.bindLoc = Vector3fImmutable.ZERO;
//grab name from minionbase.
mbEnums.MinionType minionType = mbEnums.MinionType.ContractToMinionMap.get(guardCaptain.contract.getContractID());
if (minionType != null) {
String rank;
if (guardCaptain.getRank() < 3)
rank = MBServerStatics.JUNIOR;
else if (guardCaptain.getRank() < 6)
rank = "";
else if (guardCaptain.getRank() == 6)
rank = MBServerStatics.VETERAN;
else
rank = MBServerStatics.ELITE;
minionMobile.lastName = rank + " " + minionType.getRace() + " " + minionType.getName();
}
// Configure and spawn minion
minionMobile.runAfterLoad();
DbManager.addToCache(minionMobile);
minionMobile.setLoc(minionMobile.bindLoc);
minionMobile.despawn();
guardCaptain.minions.add(minionMobile.getObjectUUID());
return minionMobile;
}
public static synchronized Mob createSiegeMinion(NPC artyCaptain, int loadID) {
Mob siegeMinion;
siegeMinion = new Mob();
siegeMinion.currentID = (--Mob.staticID);
siegeMinion.level = 1;
siegeMinion.loadID = loadID;
siegeMinion.guildUUID = artyCaptain.guildUUID;
siegeMinion.equipmentSetID = 0;
siegeMinion.buildingUUID = artyCaptain.buildingUUID;
siegeMinion.guardCaptain = artyCaptain;
siegeMinion.parentZoneUUID = artyCaptain.parentZoneUUID;
siegeMinion.behaviourType = MobBehaviourType.SiegeEngine;
siegeMinion.agentType = AIAgentType.SIEGEENGINE;
siegeMinion.bindLoc = Vector3fImmutable.ZERO;
siegeMinion.spawnDelay = minionSpawnDelay(60 * 15);
siegeMinion.runAfterLoad();
DbManager.addToCache(siegeMinion);
siegeMinion.setLoc(siegeMinion.bindLoc);
siegeMinion.despawn();
artyCaptain.minions.add(siegeMinion.getObjectUUID());
return siegeMinion;
}
public static synchronized Mob createPetMinion(int loadID, Zone parent, PlayerCharacter petOwner, short level) {
if (petOwner == null)
return null;
Mob petMinion = new Mob();
petMinion.currentID = (--Mob.staticID);
petMinion.level = (short) (level + 20);
petMinion.loadID = loadID;
petMinion.bindLoc = petOwner.getLoc();
petMinion.loc = petOwner.getLoc();
petMinion.guardCaptain = petOwner;
petMinion.parentZoneUUID = parent.getObjectUUID();
petMinion.walkMode = false;
petMinion.healthMax = MobBase.getMobBase(loadID).getHealthMax() * (petMinion.level * 0.5f);
petMinion.health.set(petMinion.healthMax);
petMinion.behaviourType = MobBehaviourType.Pet1;
petMinion.agentType = AIAgentType.PET;
petMinion.firstName = "";
petMinion.lastName = "";
petMinion.despawned = false;
petMinion.runAfterLoad();
// Necro pets count as pets too: DRU-066 targets Pet and requires NECROPETSTATS
petMinion.setObjectTypeMask(petMinion.getObjectTypeMask() | MBServerStatics.MASK_PET);
DbManager.addToCache(petMinion);
petMinion.setLoc(petMinion.bindLoc);
return petMinion;
}
public static Mob getMob(int id) {
if (id == 0)
return null;
Mob mob = (Mob) DbManager.getFromCache(GameObjectType.Mob, id);
if (mob != null)
return mob;
return DbManager.MobQueries.GET_MOB(id);
}
public static Mob getFromCache(int id) {
return (Mob) DbManager.getFromCache(GameObjectType.Mob, id);
}
private static float getModifiedAmount(CharacterSkill skill) {
if (skill == null)
return 0f;
return skill.getModifiedAmount();
}
public static void submitUpgradeJob(Mob mob) {
if (mob.getUpgradeDateTime() == null) {
Logger.error("Failed to get Upgrade Date");
return;
}
// Submit upgrade job for future date or current instant
if (mob.getUpgradeDateTime().isAfter(DateTime.now()))
JobScheduler.getInstance().scheduleJob(new UpgradeNPCJob(mob), mob.getUpgradeDateTime().getMillis());
else
JobScheduler.getInstance().scheduleJob(new UpgradeNPCJob(mob), 0);
}
public static int getUpgradeTime(Mob mob) {
if (mob.getRank() < 7)
return (mob.getRank() * 8);
return 0;
}
public static int getUpgradeCost(Mob mob) {
int upgradeCost;
upgradeCost = Integer.MAX_VALUE;
if (mob.getRank() < 7)
return (mob.getRank() * 100650) + 21450;
return upgradeCost;
}
public static void setUpgradeDateTime(Mob mob, DateTime upgradeDateTime) {
if (!DbManager.MobQueries.updateUpgradeTime(mob, upgradeDateTime)) {
Logger.error("Failed to set upgradeTime for building " + mob.currentID);
return;
}
mob.upgradeDateTime = upgradeDateTime;
}
/*
* Getters
*/
@Override
public int getDBID() {
return this.dbID;
}
public int getLoadID() {
return loadID;
}
/*
* Serialization
*/
@Override
public int getObjectUUID() {
return currentID;
}
public float getSpawnRadius() {
return this.spawnRadius;
}
public String getSpawnTimeAsString() {
if (this.spawnDelay == 0)
return MBServerStatics.DEFAULT_SPAWN_TIME_MS / 1000 + " seconds (Default)";
else
return this.spawnDelay + " seconds";
}
@Override
public MobBase getMobBase() {
return this.mobBase;
}
public int getMobBaseID() {
return this.mobBase.getObjectUUID();
}
public Vector3fImmutable getTrueBindLoc() {
return this.bindLoc;
}
public Zone getParentZone() {
return this.parentZone;
}
@Override
public int getGuildUUID() {
if (this.guild == null)
return 0;
return this.guild.getObjectUUID();
}
@Override
public Vector3fImmutable getBindLoc() {
if (this.isPet() && !this.behaviourType.equals(MobBehaviourType.SiegeEngine))
return this.guardCaptain != null ? this.guardCaptain.getLoc() : this.getLoc();
else
return this.bindLoc;
}
public void calculateModifiedStats() {
float strVal = this.mobBase.getMobBaseStats().getBaseStr();
float dexVal = this.mobBase.getMobBaseStats().getBaseDex();
float conVal = this.mobBase.getMobBaseStats().getBaseCon();
float intVal = this.mobBase.getMobBaseStats().getBaseInt();
float spiVal = this.mobBase.getMobBaseStats().getBaseSpi();
// TODO modify for equipment
if (this.bonuses != null) {
// modify for effects
strVal += this.bonuses.getFloat(ModType.Attr, SourceType.Strength);
dexVal += this.bonuses.getFloat(ModType.Attr, SourceType.Dexterity);
conVal += this.bonuses.getFloat(ModType.Attr, SourceType.Constitution);
intVal += this.bonuses.getFloat(ModType.Attr, SourceType.Intelligence);
spiVal += this.bonuses.getFloat(ModType.Attr, SourceType.Spirit);
// apply dex penalty for armor
// modify percent amounts. DO THIS LAST!
strVal *= (1 + this.bonuses.getFloatPercentAll(ModType.Attr, SourceType.Strength));
dexVal *= (1 + this.bonuses.getFloatPercentAll(ModType.Attr, SourceType.Dexterity));
conVal *= (1 + this.bonuses.getFloatPercentAll(ModType.Attr, SourceType.Constitution));
intVal *= (1 + this.bonuses.getFloatPercentAll(ModType.Attr, SourceType.Intelligence));
spiVal *= (1 + this.bonuses.getFloatPercentAll(ModType.Attr, SourceType.Spirit));
}
// Set current stats
this.statStrCurrent = (strVal < 1) ? (short) 1 : (short) strVal;
this.statDexCurrent = (dexVal < 1) ? (short) 1 : (short) dexVal;
this.statConCurrent = (conVal < 1) ? (short) 1 : (short) conVal;
this.statIntCurrent = (intVal < 1) ? (short) 1 : (short) intVal;
this.statSpiCurrent = (spiVal < 1) ? (short) 1 : (short) spiVal;
}
@Override
public float getSpeed() {
float bonus = 1;
if (this.bonuses != null)
// get rune and effect bonuses
bonus *= (1 + this.bonuses.getFloatPercentAll(ModType.Speed, SourceType.None));
if (this.isPlayerGuard())
switch (this.mobBase.getObjectUUID()) {
case 2111:
if (this.isWalk())
if (this.isCombat())
return Guards.HumanArcher.getWalkCombatSpeed() * bonus;
else
return Guards.HumanArcher.getWalkSpeed() * bonus;
else
return Guards.HumanArcher.getRunSpeed() * bonus;
case 14103:
if (this.isWalk())
if (this.isCombat())
return Guards.UndeadArcher.getWalkCombatSpeed() * bonus;
else
return Guards.UndeadArcher.getWalkSpeed() * bonus;
else
return Guards.UndeadArcher.getRunSpeed() * bonus;
}
//return combat speeds
//not combat return normal speeds
if (this.isCombat())
if (this.isWalk()) {
if (this.mobBase.getWalkCombat() <= 0)
return MBServerStatics.MOB_SPEED_WALKCOMBAT * bonus;
return this.mobBase.getWalkCombat() * bonus;
} else {
if (this.mobBase.getRunCombat() <= 0)
return MBServerStatics.MOB_SPEED_RUNCOMBAT * bonus;
return this.mobBase.getRunCombat() * bonus;
}
else if (this.isWalk()) {
if (this.mobBase.getWalk() <= 0)
return MBServerStatics.MOB_SPEED_WALK * bonus;
return this.mobBase.getWalk() * bonus;
} else {
if (this.mobBase.getRun() <= 0)
return MBServerStatics.MOB_SPEED_RUN * bonus;
return this.mobBase.getRun() * bonus;
}
}
@Override
public float getPassiveChance(String type, int AttackerLevel, boolean fromCombat) {
//TODO add this later for dodge
return 0f;
}
/*
* Database
*/
/**
* @ Kill this Character
*/
@Override
public void killCharacter(AbstractCharacter attacker) {
this.stopMovement(this.getMovementLoc());
if (attacker != null)
if (attacker.getObjectType() == GameObjectType.PlayerCharacter) {
Group g = GroupManager.getGroup((PlayerCharacter) attacker);
// Give XP, now handled inside the Experience Object
if (!this.isPet() && !this.isNecroPet() && !(this.agentType.equals(AIAgentType.PET)) && !this.isPlayerGuard())
Experience.doExperience((PlayerCharacter) attacker, this, g);
} else if (attacker.getObjectType().equals(GameObjectType.Mob)) {
Mob mobAttacker = (Mob) attacker;
if (mobAttacker.isPet()) {
PlayerCharacter owner = (PlayerCharacter) mobAttacker.guardCaptain;
if (owner != null)
if (!this.isPet() && !this.isNecroPet() && !(this.agentType.equals(AIAgentType.PET)) && !this.isPlayerGuard()) {
Group g = GroupManager.getGroup(owner);
// Give XP, now handled inside the Experience Object
Experience.doExperience(owner, this, g);
}
}
}
killCleanup();
}
public void updateLocation() {
if (!this.isMoving())
return;
if (this.isAlive() == false || this.getBonuses().getBool(ModType.Stunned, SourceType.None) || this.getBonuses().getBool(ModType.CannotMove, SourceType.None)) {
//Target is stunned or rooted. Don't move
this.stopMovement(this.getMovementLoc());
return;
}
Vector3fImmutable newLoc = this.getMovementLoc();
if (newLoc.equals(this.getEndLoc())) {
this.stopMovement(newLoc);
return;
//Next upda
}
setLoc(newLoc);
//Next update will be end Loc, lets stop him here.
}
@Override
public void killCharacter(String reason) {
killCleanup();
}
private void killCleanup() {
Dispatch dispatch;
try {
//resync corpses
if (this.behaviourType.equals(MobBehaviourType.SiegeEngine)) {
this.deathTime = System.currentTimeMillis();
try {
this.clearEffects();
} catch (Exception e) {
Logger.error(e.getMessage());
}
this.setCombatTarget(null);
this.hasLoot = false;
this.playerAgroMap.clear();
if (this.behaviourType.ordinal() == mbEnums.MobBehaviourType.GuardMinion.ordinal())
this.spawnDelay = (int) (-2.500 * this.guardCaptain.building.getRank() + 22.5) * 60;
if (this.isPet()) {
PlayerCharacter petOwner = (PlayerCharacter) this.guardCaptain;
if (petOwner != null) {
this.guardCaptain = null;
petOwner.setPet(null);
PetMsg petMsg = new PetMsg(5, null);
dispatch = Dispatch.borrow((PlayerCharacter) this.guardCaptain, petMsg);
DispatchManager.dispatchMsgDispatch(dispatch, mbEnums.DispatchChannel.PRIMARY);
}
}
} else if (this.isPet() || this.isNecroPet()) {
this.setCombatTarget(null);
this.hasLoot = false;
ZoneManager.seaFloor.zoneMobSet.remove(this);
try {
this.clearEffects();
} catch (Exception e) {
Logger.error(e.getMessage());
}
this.playerAgroMap.clear();
WorldGrid.RemoveWorldObject(this);
DbManager.removeFromCache(this);
PlayerCharacter petOwner = (PlayerCharacter) this.guardCaptain;
if (petOwner != null) {
this.guardCaptain = null;
petOwner.setPet(null);
PetMsg petMsg = new PetMsg(5, null);
dispatch = Dispatch.borrow(petOwner, petMsg);
DispatchManager.dispatchMsgDispatch(dispatch, mbEnums.DispatchChannel.PRIMARY);
}
} else {
//cleanup effects
playerAgroMap.clear();
if (!this.isPlayerGuard() && this.charItemManager.equipped != null)
LootManager.GenerateEquipmentDrop(this);
}
try {
this.clearEffects();
} catch (Exception e) {
Logger.error(e.getMessage());
}
this.combat = false;
this.walkMode = true;
this.setCombatTarget(null);
this.hasLoot = this.charItemManager.getInventoryCount() > 0;
} catch (Exception e) {
Logger.error(e);
}
this.updateLocation();
}
public void respawn() {
this.despawned = false;
this.setCombatTarget(null);
this.setHealth(this.healthMax);
this.stamina.set(this.staminaMax);
this.mana.set(this.manaMax);
this.combat = false;
this.walkMode = true;
this.setCombatTarget(null);
this.isAlive.set(true);
this.deathTime = 0;
this.lastBindLoc = this.bindLoc;
this.setLoc(this.lastBindLoc);
this.stopMovement(this.lastBindLoc);
NPCManager.applyMobbaseEffects(this);
this.recalculateStats();
this.setHealth(this.healthMax);
if (this.building == null && this.guardCaptain != null && ((Mob) this.guardCaptain).behaviourType.equals(MobBehaviourType.GuardCaptain))
this.building = this.guardCaptain.building;
this.loadInventory();
this.updateLocation();
}
public void despawn() {
this.despawned = true;
WorldGrid.RemoveWorldObject(this);
this.charItemManager.clearInventory();
}
@Override
public boolean canBeLooted() {
return !this.isAlive();
}
public int getTypeMasks() {
if (this.mobBase == null)
return 0;
return this.mobBase.getTypeMasks();
}
/**
* Clears and sets the inventory of the Mob. Must be called every time the
* mob is spawned or respawned.
*/
public void loadInventory() {
if (!MBServerStatics.ENABLE_MOB_LOOT)
return;
this.charItemManager.clearInventory();
this.charItemManager.clearEquip();
// Reload equipment set
this.charItemManager.equipped = MobBase.loadEquipmentSet(this.equipmentSetID);
// Only generate loot for mobiles
if (!this.agentType.equals(AIAgentType.MOBILE))
return;
LootManager.GenerateMobLoot(this);
}
@Override
public void updateDatabase() {
}
public void refresh() {
if (this.isAlive())
WorldGrid.updateObject(this);
}
public void recalculateStats() {
if (this.isPlayerGuard()) {
NPCManager.setMaxHealthForGuard(this);
NPCManager.setAttackRatingForGuard(this);
NPCManager.setDefenseForGuard(this);
NPCManager.setDamageAndSpeedForGuard(this);
NPCManager.applyGuardStanceModifiers(this);
} else {
AbstractCharacter.calculateAtrDefenseDamage(this);
calculateMaxHealthManaStamina();
}
calculateModifiedStats();
if (this.isSiege())
this.healthMax = 10000;
Resists.calculateResists(this);
}
public void calculateMaxHealthManaStamina() {
float h;
float m;
float s;
h = this.mobBase.getHealthMax();
if (this.isPet()) {
h = this.level * 0.5f * 120;
}
m = this.statSpiCurrent;
s = this.statConCurrent;
// Apply any bonuses from runes and effects
if (this.bonuses != null) {
h += this.bonuses.getFloat(ModType.HealthFull, SourceType.None);
m += this.bonuses.getFloat(ModType.ManaFull, SourceType.None);
s += this.bonuses.getFloat(ModType.StaminaFull, SourceType.None);
//apply effects percent modifiers. DO THIS LAST!
h *= (1 + this.bonuses.getFloatPercentAll(ModType.HealthFull, SourceType.None));
m *= (1 + this.bonuses.getFloatPercentAll(ModType.ManaFull, SourceType.None));
s *= (1 + this.bonuses.getFloatPercentAll(ModType.StaminaFull, SourceType.None));
}
// Set max health, mana and stamina
if (h > 0)
this.healthMax = h;
else
this.healthMax = 1;
if (m > -1)
this.manaMax = m;
else
this.manaMax = 0;
if (s > -1)
this.staminaMax = s;
else
this.staminaMax = 0;
// Update health, mana and stamina if needed
if (this.getHealth() > this.healthMax)
this.setHealth(this.healthMax);
if (this.mana.get() > this.manaMax)
this.mana.set(this.manaMax);
if (this.stamina.get() > this.staminaMax)
this.stamina.set(staminaMax);
}
@Override
public void runAfterLoad() {
this.setObjectTypeMask(MBServerStatics.MASK_MOB | this.getTypeMasks());
if (ConfigManager.serverType.equals(ServerType.LOGINSERVER))
return;
this.mobBase = MobBase.getMobBase(loadID);
this.building = BuildingManager.getBuilding(this.buildingUUID);
// Configure AI related values
switch (this.behaviourType) {
case GuardCaptain:
this.agentType = AIAgentType.GUARDCAPTAIN;
this.spawnDelay = 600;
if (this.building == null)
Logger.error("Captain : " + this.getObjectUUID() + " missing building " + this.buildingUUID);
this.guardedCity = ZoneManager.getCityAtLocation(this.building.getLoc());
break;
case GuardWallArcher:
this.agentType = AIAgentType.GUARDWALLARCHER;
this.spawnDelay = 450;
if (this.building == null)
Logger.error("Wall Archer : " + this.getObjectUUID() + " missing building " + this.buildingUUID);
this.guardedCity = ZoneManager.getCityAtLocation(this.building.getLoc());
break;
}
// Default to the mobbase for AI if nothing is in mob field to override.
if (this.behaviourType == null || this.behaviourType.equals(MobBehaviourType.None))
this.behaviourType = this.getMobBase().fsm;
if (this.behaviourType == null)
this.behaviourType = MobBehaviourType.None;
if (this.contractUUID == 0)
this.contract = null;
else
this.contract = DbManager.ContractQueries.GET_CONTRACT(this.contractUUID);
// Setup equipset for contract
if (this.contract != null)
this.equipmentSetID = this.contract.equipmentSet;
// Mobiles default to the building guild.
if (this.building != null)
this.guild = this.building.getGuild();
else
this.guild = Guild.getGuild(guildUUID);
if (this.guild == null)
this.guild = Guild.getErrantGuild();
if (this.firstName.isEmpty())
this.firstName = this.mobBase.getFirstName();
if (this.contract != null)
if (this.lastName.isEmpty())
this.lastName = this.getContract().getName();
this.healthMax = this.mobBase.getHealthMax();
this.manaMax = 0;
this.staminaMax = 0;
this.setHealth(this.healthMax);
this.mana.set(this.manaMax);
this.stamina.set(this.staminaMax);
// Don't override level for guard minions or pets
if (this.contract == null)
if (!this.agentType.equals(AIAgentType.GUARDMINION) && !this.agentType.equals(AIAgentType.PET))
this.level = (short) this.mobBase.getLevel();
//set bonuses
this.bonuses = new PlayerBonuses(this);
//TODO set these correctly later
this.rangeHandOne = this.mobBase.getAttackRange();
this.rangeHandTwo = -1;
this.minDamageHandOne = (int) this.mobBase.getDamageMin();
this.maxDamageHandOne = (int) this.mobBase.getDamageMax();
this.minDamageHandTwo = 0;
this.maxDamageHandTwo = 0;
this.atrHandOne = this.mobBase.getAttackRating();
this.defenseRating = (short) this.mobBase.getDefenseRating();
this.isActive = true;
// Configure parent zone adding this NPC to the
// zone collection
this.parentZone = ZoneManager.getZoneByUUID(this.parentZoneUUID);
this.parentZone.zoneMobSet.remove(this);
this.parentZone.zoneMobSet.add(this);
// Handle Mobiles within buildings
if (this.building == null) {
// Do not adjust a pet's bindloc.
if (!this.agentType.equals(AIAgentType.PET))
this.bindLoc = this.parentZone.getLoc().add(this.bindLoc);
} else {
// Mobiles inside buildings are offset from it not the zone
// with the exceptions being mobiles
// with a contract.
if (this.contract != null || this.behaviourType.equals(MobBehaviourType.SiegeEngine))
NPCManager.slotCharacterInBuilding(this);
else
this.bindLoc = building.getLoc().add(bindLoc);
}
// Setup location for this Mobile
this.setLoc(bindLoc);
this.endLoc = new Vector3fImmutable(bindLoc);
// Initialize inventory
this.charItemManager.load();
this.loadInventory();
if (this.equipmentSetID != 0)
this.charItemManager.equipped = MobBase.loadEquipmentSet(this.equipmentSetID);
// Combine mobbase and mob aggro arrays into one bitvector
//skip for pets
if (this.isPet() == false && this.isNecroPet() == false) {
if (this.getMobBase().notEnemy.size() > 0)
this.notEnemy.addAll(this.getMobBase().notEnemy);
if (this.getMobBase().enemy.size() > 0)
this.enemy.addAll(this.getMobBase().enemy);
}
// Load skills, powers and effects
NPCManager.applyMobbaseEffects(this);
NPCManager.applyEquipmentResists(this);
NPCManager.applyRuneSkills(this, this.getMobBaseID());
this.recalculateStats();
this.setHealth(this.healthMax);
// Set bounds for this mobile
Bounds mobBounds = Bounds.borrow();
mobBounds.setBounds(this.getLoc());
this.setBounds(mobBounds);
//assign 5 random patrol points for regular mobs
if (this.agentType.equals(AIAgentType.MOBILE))
NPCManager.AssignPatrolPoints(this);
// Load minions for guard captain.
if (this.agentType.equals(AIAgentType.GUARDCAPTAIN))
DbManager.MobQueries.LOAD_GUARD_MINIONS(this);
this.deathTime = 0;
}
@Override
protected ConcurrentHashMap<Integer, CharacterPower> initializePowers() {
return new ConcurrentHashMap<>(MBServerStatics.CHM_INIT_CAP, MBServerStatics.CHM_LOAD, MBServerStatics.CHM_THREAD_LOW);
}
public boolean canSee(AbstractCharacter target) {
return this.mobBase.getSeeInvis() >= target.hidden;
}
public int getBuildingID() {
return buildingUUID;
}
public void setBuildingID(int buildingID) {
this.buildingUUID = buildingID;
}
public boolean isSiege() {
return this.behaviourType.equals(MobBehaviourType.SiegeEngine);
}
public void setGuardCaptain(AbstractCharacter guardCaptain) {
this.guardCaptain = guardCaptain;
}
public boolean isNecroPet() {
return this.mobBase.isNecroPet();
}
public void handleDirectAggro(AbstractCharacter ac) {
if (!ac.getObjectType().equals(GameObjectType.PlayerCharacter))
return;
// siege engines only switch targets on damage, see AbstractCharacter.modifyHealth
if (this.isSiege())
return;
if (this.getCombatTarget() == null)
this.setCombatTarget(ac);
}
public void setRank(int newRank) {
DbManager.MobQueries.SET_PROPERTY(this, "mob_level", newRank);
this.level = (short) newRank;
this.recalculateStats();
this.setHealth(this.healthMax);
}
public boolean isRanking() {
return this.upgradeDateTime != null;
}
public long getNextAttackTime() {
return nextAttackTime;
}
public void setDeathTime(long deathTime) {
this.deathTime = deathTime;
}
public boolean isHasLoot() {
return hasLoot;
}
public void setWeaponPower(DeferredPowerJob weaponPower) {
this.weaponPower = weaponPower;
}
public DateTime getUpgradeDateTime() {
lock.readLock().lock();
try {
return upgradeDateTime;
} finally {
lock.readLock().unlock();
}
}
public Contract getContract() {
return contract;
}
public void setContract(Contract contract) {
this.contract = contract;
}
public boolean isPlayerGuard() {
return EnumSet.of(AIAgentType.GUARDCAPTAIN, AIAgentType.GUARDMINION, AIAgentType.GUARDWALLARCHER).contains(this.agentType);
}
public int getLastMobPowerToken() {
return lastMobPowerToken;
}
public void setLastMobPowerToken(int lastMobPowerToken) {
this.lastMobPowerToken = lastMobPowerToken;
}
public boolean isLootSync() {
return lootSync;
}
public void setLootSync(boolean lootSync) {
this.lootSync = lootSync;
}
public String getNameOverride() {
return firstName + " " + lastName;
}
public void processUpgradeMob(PlayerCharacter player) {
lock.writeLock().lock();
try {
// Cannot upgrade an npc not within a building
if (building == null)
return;
// Cannot upgrade an npc at max rank
if (this.getRank() == 7)
return;
// Cannot upgrade an npc who is currently ranking
if (this.isRanking())
return;
int rankCost = Mob.getUpgradeCost(this);
// SEND NOT ENOUGH GOLD ERROR
if (rankCost > building.getStrongboxValue()) {
sendErrorPopup(player, 127);
return;
}
try {
if (!building.transferGold(-rankCost, false))
return;
DateTime dateToUpgrade = DateTime.now().plusHours(Mob.getUpgradeTime(this));
Mob.setUpgradeDateTime(this, dateToUpgrade);
// Schedule upgrade job
Mob.submitUpgradeJob(this);
} catch (Exception e) {
PlaceAssetMsg.sendPlaceAssetError(player.getClientConnection(), 1, "A Serious error has occurred. Please post details for to ensure transaction integrity");
}
} catch (Exception e) {
Logger.error(e);
} finally {
lock.writeLock().unlock();
}
}
public Boolean isGuard() {
switch (this.behaviourType) {
case GuardMinion:
case GuardCaptain:
case GuardWallArcher:
case HamletGuard:
case SimpleStandingGuard:
return true;
}
return false;
}
@Override
public long getDelay(@NotNull TimeUnit unit) {
long timeRemaining = this.respawnTime - System.currentTimeMillis();
return unit.convert(timeRemaining, TimeUnit.MILLISECONDS);
}
@Override
public int compareTo(@NotNull Delayed o) {
return toIntExact(this.respawnTime - ((Mob) o).respawnTime);
}
}