Compare commits

...
19 Commits
Author SHA1 Message Date
MagicBotandClaude Opus 5.5 40799f96f6 Mark summoned pets with MASK_PET
Summoned pets (not necro pets) now carry MASK_PET, so pet-targeted
AOE powers can find them. The checkForAggro pet scan is held off with
PET_AGGRO_ENABLED until it checks rune enemy lists, filters targets
and keeps pet targets (plan-mask-pet.md steps 2-4).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-15 09:19:36 -04:00
MagicBotandClaude Opus 5.5 5262a602da Keep siege engine target when a new player loads it
InterestManager cleared the combat target of every live mob loaded for
a new player, so a siege engine firing at a wall dropped its target
whenever someone entered load range. Siege engines now keep it.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-15 09:05:24 -04:00
MagicBotandClaude Opus 5.5 5b80614376 Restore regular mob MASK_PET aggro scan
The scan was removed as dead code, but creature runes list Pet in
rune_enemy_monster_types, so monsters are meant to aggro summoned pets.
Restored unchanged; it stays inactive until MASK_PET is set from the
rune data.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-15 09:00:27 -04:00
MagicBotandClaude Opus 5.5 b60f2754f1 Remove siege and pet auto-aggro scans
Guard MASK_SIEGE and regular mob MASK_PET scans were dead code: no mob
ever carries either mask. Removed so they cannot start auto-aggroing
siege engines if the masks are wired up later. Safehold guards no
longer aggro siege engines. Guards still respond to a siege engine
damaging a city building.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-15 08:53:57 -04:00
MagicBotandClaude Opus 5.5 89cdf5655d Flag players hostile for damaging siege engines on a city grid
Damaging a siege engine on a city grid adds the attacker (or the owning
player, for pets) to that city's outlaw list, unless they are in the
city's nation or an allied nation.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-15 08:53:24 -04:00
MagicBotandClaude Opus 5.5 c9208000bb Pull siege engine aggro on damage only
A siege engine switches to whoever damages it, player or pet, even when
it already has a target. Pre-roll melee retaliation, harmful-power
direct aggro (which also fires on misses) and Taunt no longer change a
siege engine's target. The attack timer is untouched, so a switched
engine still waits for its next cycle before firing.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-15 08:52:56 -04:00
MagicBotandClaude Opus 5.5 531a2d028b Drop siege engine targets only on death or attack range
Siege engines dropped player targets at 100 units, at the 200 unit
drop-aggro range, or when the player was missing from playerAgroMap,
all well inside their 350 attack range. They now hold a character
target until it dies, can't be seen, or leaves getRange().

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-15 08:52:09 -04:00
MagicBotandClaude Opus 5.5 ca33c5a14c Keep siege engines alive and targeting when released
Owner death, logout or invisibility dismissed a commanded siege engine
by clearing its target and marking it dead. Released engines now revert
to MOBILE in place, stay alive and keep their current target so any
nation member can re-command them with a Rod of Command. Commanding an
engine no longer clears the target of the old or new engine.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-15 08:51:38 -04:00
MagicBot 5868530414 mobai aligned with rune data and client. 2026-10-15 07:31:30 -04:00
MagicBotandClaude Opus 5.5 66a731005e Use race rune natural attack damage for unarmed players
Unarmed base damage was a hardcoded 1-3 (TODO). Read it from the race
rune natural attack for the swinging hand (1-4 for all races), then
scale it with the existing formula, matching the client (0x46D200).
Falls back to 1-3 if the rune has no natural attack.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-15 07:24:33 -04:00
MagicBotandClaude Opus 5.5 330db50985 Use rune ATTACK for regular mob ATR
The client (0x466570) gives mobiles their ATR straight from the rune
ATTACK. Read rune_attack from the rune template, falling back to the
mobbase atr when the rune has none.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-15 07:15:26 -04:00
MagicBotandClaude Opus 5.5 12c08c940a Use rune MINDAMAGE/MAXDAMAGE for regular mob damage
The client (ArcMobile, 0x46D200) takes mob damage straight from the rune
MINDAMAGE/MAXDAMAGE with no stat scaling; natural attack damage is only
scaled for non-mobile characters. Read rune_min_damage/rune_max_damage
from the rune template, falling back to the mobbase row when the rune
has none. 2689 of 2770 mobbase rows already match the rune.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-15 07:13:52 -04:00
MagicBotandClaude Opus 5.5 b21ccdbb4c Use rune natural attack range for unarmed mobs
Unarmed mobs now add their rune natural attack range in processAttack
and use it in getRange. Most melee runes have 0, so they are unchanged;
ranged plant mobs (Gasser, Spitter, Needler, ...) get 150, Shuggra 50.
Natural range is not subject to the unverified 80 clamp.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-15 07:05:25 -04:00
MagicBotandClaude Opus 5.5 a708a1f0b2 Use rune natural attack range for siege engines
Siege engine range was hardcoded to 300. Read it from the rune natural
attack instead (350 for Trebuchet, Mangonel and Ballista), matching the
client's RecalcMinWeaponRange. Falls back to 300 if the rune has none.
getNaturalAttack now returns the whole entry so speed and range share
the lookup.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-15 07:04:11 -04:00
MagicBotandClaude Opus 5.5 b525a7a3ab Document verified rune natural attack layout
Field layout of rune_naturalattacks confirmed in the client (IDA): it is
the same attack info struct as a weapon, so [0] is WEPSPEED.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-15 07:01:57 -04:00
MagicBotandClaude Opus 5.5 b690195233 Use race rune natural attack speed for unarmed players
Extend getNaturalAttackSpeed to resolve the rune from the attacker type:
mobbase rune for mobs, race rune for players. Unarmed players now swing
at their race rune speed (20 = 2.0 s per hand) instead of a flat 5000 ms.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-15 06:56:21 -04:00
MagicBotandClaude Opus 5.5 9aa44d4e9a Use rune natural attack speed for unarmed mobs
Unarmed mobs used a flat 5000 ms swing delay. Parse rune_naturalattacks
from the rune template and use its speed (x100 ms) instead, with the
AttackDelay modifier and 1.0 s floor applied as for weapons. Siege
engines now fire every 17.5 s (speed 175); most creatures swing every 3.5 s.
Falls back to 5000 ms when the rune has no natural attack.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-15 06:53:50 -04:00
MagicBot ab1c3f12d3 mobai null resolution 2026-10-14 15:22:38 -04:00
MagicBot 6017641896 mobai null resolution 2026-10-14 14:36:07 -04:00
10 changed files with 307 additions and 74 deletions

No files matched your search

@@ -468,7 +468,12 @@ public enum InterestManager implements Runnable {
continue; continue;
awonpc.playerAgroMap.put(player.getObjectUUID(), 0f); awonpc.playerAgroMap.put(player.getObjectUUID(), 0f);
// siege engines keep firing at their target when a new player loads them
if (!awonpc.isSiege())
((Mob) awonpc).setCombatTarget(null); ((Mob) awonpc).setCombatTarget(null);
lcm = new LoadCharacterMsg(awonpc, PlayerCharacter.hideNonAscii()); lcm = new LoadCharacterMsg(awonpc, PlayerCharacter.hideNonAscii());
} else if ((awo.getObjectTypeMask() & MBServerStatics.MASK_NPC) != 0) { } else if ((awo.getObjectTypeMask() & MBServerStatics.MASK_NPC) != 0) {
+110 -5
View File
@@ -14,6 +14,7 @@ import engine.jobs.AttackJob;
import engine.jobs.DeferredPowerJob; import engine.jobs.DeferredPowerJob;
import engine.mbEnums; import engine.mbEnums;
import engine.net.client.ClientConnection; import engine.net.client.ClientConnection;
import engine.net.client.msg.PowerProjectileMsg;
import engine.net.client.msg.TargetedActionMsg; import engine.net.client.msg.TargetedActionMsg;
import engine.net.client.msg.UpdateStateMsg; import engine.net.client.msg.UpdateStateMsg;
import engine.objects.*; import engine.objects.*;
@@ -146,11 +147,19 @@ public enum CombatManager {
rangeMod += bonus.getFloatPercentAll(mbEnums.ModType.WeaponRange, mbEnums.SourceType.None); rangeMod += bonus.getFloatPercentAll(mbEnums.ModType.WeaponRange, mbEnums.SourceType.None);
attackRange += weapon.template.item_weapon_max_range * rangeMod; attackRange += weapon.template.item_weapon_max_range * rangeMod;
} else if (attacker.getObjectType().equals(mbEnums.GameObjectType.Mob)) {
//unarmed mob, add the natural attack range from its rune (0 for most melee mobs)
if (bonus != null)
rangeMod += bonus.getFloatPercentAll(mbEnums.ModType.WeaponRange, mbEnums.SourceType.None);
attackRange += getNaturalAttackRange(attacker, slot) * rangeMod;
} }
if (attacker.getObjectType().equals(mbEnums.GameObjectType.Mob)) if (attacker.getObjectType().equals(mbEnums.GameObjectType.Mob))
if (((Mob) attacker).isSiege()) if (((Mob) attacker).isSiege())
attackRange = 300; attackRange = getSiegeRange((Mob) attacker);
float distanceSquared = attacker.loc.distanceSquared(target.loc); float distanceSquared = attacker.loc.distanceSquared(target.loc);
@@ -214,6 +223,22 @@ public enum CombatManager {
wepSpeed = 10; //Old was 10, but it can be reached lower with legit buffs,effects. wepSpeed = 10; //Old was 10, but it can be reached lower with legit buffs,effects.
delay = (long)wepSpeed * 100L; delay = (long)wepSpeed * 100L;
} else {
//unarmed, use the natural attack speed from the mob or race rune
float naturalSpeed = getNaturalAttackSpeed(attacker, slot);
if (naturalSpeed > 0) {
if (attacker.getBonuses() != null && attacker.getBonuses().getFloatPercentAll(mbEnums.ModType.AttackDelay, mbEnums.SourceType.None) != 0f) //add effects speed bonus
naturalSpeed *= (1 + attacker.getBonuses().getFloatPercentAll(mbEnums.ModType.AttackDelay, mbEnums.SourceType.None));
if (naturalSpeed < 10)
naturalSpeed = 10;
delay = (long) naturalSpeed * 100L;
}
} }
if (attacker.getObjectType().equals(mbEnums.GameObjectType.Mob)) if (attacker.getObjectType().equals(mbEnums.GameObjectType.Mob))
@@ -232,7 +257,8 @@ public enum CombatManager {
//handle retaliate //handle retaliate
if (AbstractCharacter.IsAbstractCharacter(target)) { //siege engines only retaliate on damage, see AbstractCharacter.modifyHealth
if (AbstractCharacter.IsAbstractCharacter(target) && !(target.getObjectType().equals(mbEnums.GameObjectType.Mob) && ((Mob) target).isSiege())) {
if (((AbstractCharacter) target).combatTarget == null || !((AbstractCharacter) target).combatTarget.isAlive()) { if (((AbstractCharacter) target).combatTarget == null || !((AbstractCharacter) target).combatTarget.isAlive()) {
((AbstractCharacter) target).combatTarget = attacker; ((AbstractCharacter) target).combatTarget = attacker;
if (target.getObjectType().equals(mbEnums.GameObjectType.PlayerCharacter) && ((AbstractCharacter) target).isCombat()) if (target.getObjectType().equals(mbEnums.GameObjectType.PlayerCharacter) && ((AbstractCharacter) target).isCombat())
@@ -319,7 +345,7 @@ public enum CombatManager {
DispatchManager.sendToAllInRange(attacker, msg); DispatchManager.sendToAllInRange(attacker, msg);
//we need to send the animation even if the attacker misses //we need to send the animation even if the attacker misses
TargetedActionMsg cmm = new TargetedActionMsg(attacker, target, (float) 0, getSwingAnimation(weapon.template, null, slot)); TargetedActionMsg cmm = new TargetedActionMsg(attacker, target, (float) 0, getSwingAnimation(weapon == null ? null : weapon.template, null, slot));
DispatchManager.sendToAllInRange(target, cmm); DispatchManager.sendToAllInRange(target, cmm);
//set auto attack job //set auto attack job
@@ -359,7 +385,7 @@ public enum CombatManager {
DispatchManager.dispatchMsgToInterestArea(target, msg, mbEnums.DispatchChannel.PRIMARY, MBServerStatics.CHARACTER_LOAD_RANGE, true, false); DispatchManager.dispatchMsgToInterestArea(target, msg, mbEnums.DispatchChannel.PRIMARY, MBServerStatics.CHARACTER_LOAD_RANGE, true, false);
//we need to send the animation even if the attacker misses //we need to send the animation even if the attacker misses
TargetedActionMsg cmm = new TargetedActionMsg(attacker, target, (float) 0, getSwingAnimation(weapon.template, null, slot)); TargetedActionMsg cmm = new TargetedActionMsg(attacker, target, (float) 0, getSwingAnimation(weapon == null ? null : weapon.template, null, slot));
DispatchManager.sendToAllInRange(target, cmm); DispatchManager.sendToAllInRange(target, cmm);
//set auto attack job //set auto attack job
setAutoAttackJob(attacker, slot, delay); setAutoAttackJob(attacker, slot, delay);
@@ -447,7 +473,7 @@ public enum CombatManager {
if (resists.immuneTo(damageType)) { if (resists.immuneTo(damageType)) {
//set auto attack job //set auto attack job
//we need to send the animation even if the attacker misses //we need to send the animation even if the attacker misses
TargetedActionMsg cmm = new TargetedActionMsg(attacker, target, (float) 0, getSwingAnimation(weapon.template, null, slot)); TargetedActionMsg cmm = new TargetedActionMsg(attacker, target, (float) 0, getSwingAnimation(weapon == null ? null : weapon.template, null, slot));
DispatchManager.sendToAllInRange(target, cmm); DispatchManager.sendToAllInRange(target, cmm);
setAutoAttackJob(attacker, slot, delay); setAutoAttackJob(attacker, slot, delay);
return; return;
@@ -479,6 +505,15 @@ public enum CombatManager {
} }
TargetedActionMsg cmm = new TargetedActionMsg(attacker, target, (float) damage, attackAnim); TargetedActionMsg cmm = new TargetedActionMsg(attacker, target, (float) damage, attackAnim);
DispatchManager.sendToAllInRange(target, cmm); DispatchManager.sendToAllInRange(target, cmm);
//siege projectile is sent once per hit, not per AI pulse
if (attacker.getObjectType().equals(mbEnums.GameObjectType.Mob) && ((Mob) attacker).isSiege()
&& target.getObjectType().equals(mbEnums.GameObjectType.Building)) {
PowerProjectileMsg ppm = new PowerProjectileMsg(attacker, target);
ppm.setRange(50);
DispatchManager.dispatchMsgToInterestArea(attacker, ppm, mbEnums.DispatchChannel.SECONDARY, MBServerStatics.CHARACTER_LOAD_RANGE, false, false);
}
} }
} }
@@ -636,6 +671,76 @@ public enum CombatManager {
} }
public static float getNaturalAttackSpeed(AbstractCharacter attacker, mbEnums.EquipSlotType slot) {
// Returns the rune natural attack speed (x100 ms) for this hand, or 0 if the rune has none.
float[] attack = getNaturalAttack(attacker, slot);
if (attack == null || attack.length < 1)
return 0;
return attack[0];
}
public static float getNaturalAttackRange(AbstractCharacter attacker, mbEnums.EquipSlotType slot) {
// Returns the rune natural attack range for this hand, or 0 if the rune has none.
float[] attack = getNaturalAttack(attacker, slot);
if (attack == null || attack.length < 2)
return 0;
return attack[1];
}
public static float getSiegeRange(Mob siegeEngine) {
// Siege engines use the range from their rune natural attack (350 for all three), 300 if the rune has none.
float range = getNaturalAttackRange(siegeEngine, mbEnums.EquipSlotType.RHELD);
if (range <= 0)
return 300;
return range;
}
public static float[] getNaturalAttack(AbstractCharacter attacker, mbEnums.EquipSlotType slot) {
// Returns the rune natural attack entry for this hand, or null if the rune has none.
// Mobs use their mobbase rune, players use their race rune.
int runeID;
ItemTemplate rune;
int hand = slot.equals(mbEnums.EquipSlotType.LHELD) ? 2 : 1;
switch (attacker.getObjectType()) {
case Mob:
runeID = ((Mob) attacker).loadID;
break;
case PlayerCharacter:
runeID = ((PlayerCharacter) attacker).getRaceID();
break;
default:
return null;
}
rune = ItemTemplate.templates.get(runeID);
if (rune == null || rune.rune_natural_attacks.isEmpty())
return null;
for (float[] attack : rune.rune_natural_attacks)
if (attack.length > 3 && (int) attack[3] == hand)
return attack;
// No entry for this hand, fall back to the first natural attack
return rune.rune_natural_attacks.get(0);
}
public static int calculatePetDamage(AbstractCharacter agent) { public static int calculatePetDamage(AbstractCharacter agent) {
//damage calc for pet //damage calc for pet
float range; float range;
+50 -56
View File
@@ -14,12 +14,10 @@ import engine.gameManager.*;
import engine.math.Vector3f; import engine.math.Vector3f;
import engine.math.Vector3fImmutable; import engine.math.Vector3fImmutable;
import engine.mbEnums; import engine.mbEnums;
import engine.mbEnums.DispatchChannel;
import engine.mobileAI.Threads.MobAIThread; import engine.mobileAI.Threads.MobAIThread;
import engine.mobileAI.Threads.ReSpawner; import engine.mobileAI.Threads.ReSpawner;
import engine.mobileAI.utilities.MovementUtilities; import engine.mobileAI.utilities.MovementUtilities;
import engine.net.client.msg.PerformActionMsg; import engine.net.client.msg.PerformActionMsg;
import engine.net.client.msg.PowerProjectileMsg;
import engine.objects.*; import engine.objects.*;
import engine.powers.ActionsBase; import engine.powers.ActionsBase;
import engine.powers.PowersBase; import engine.powers.PowersBase;
@@ -46,6 +44,11 @@ public class MobAI {
// //
// CombatManager.class implements shared combat routines for all avatars. // CombatManager.class implements shared combat routines for all avatars.
// Pet aggro scan in checkForAggro stays off until it checks rune enemy lists,
// filters its targets and keeps pet targets (MBDATA/docs/plan-mask-pet.md steps 2-4).
private static final boolean PET_AGGRO_ENABLED = false;
private static void attackTarget(Mob mob, AbstractWorldObject target) { private static void attackTarget(Mob mob, AbstractWorldObject target) {
try { try {
@@ -93,7 +96,7 @@ public class MobAI {
mob.updateLocation(); mob.updateLocation();
} catch (Exception e) { } catch (Exception e) {
Logger.info(mob.getObjectUUID() + " " + mob.getName() + " Failed At: AttackTarget" + " " + e.getMessage()); Logger.error(e, mob.getObjectUUID() + " " + mob.getName() + " Failed At: AttackTarget");
} }
} }
@@ -112,7 +115,9 @@ public class MobAI {
if (mob.behaviourType.callsForHelp) if (mob.behaviourType.callsForHelp)
mobCallForHelp(mob); mobCallForHelp(mob);
if (!MovementUtilities.inRangeDropAggro(mob, target)) { // siege engines drop targets on their own attack range in determineAction
if (!mob.isSiege() && !MovementUtilities.inRangeDropAggro(mob, target)) {
mob.setCombatTarget(null); mob.setCombatTarget(null);
return; return;
} }
@@ -133,7 +138,7 @@ public class MobAI {
target.getPet().setCombatTarget(mob); target.getPet().setCombatTarget(mob);
} catch (Exception e) { } catch (Exception e) {
Logger.info(mob.getObjectUUID() + " " + mob.getName() + " Failed At: AttackPlayer" + " " + e.getMessage()); Logger.error(e, mob.getObjectUUID() + " " + mob.getName() + " Failed At: AttackPlayer");
} }
} }
@@ -155,20 +160,10 @@ public class MobAI {
if (guard.getCombatTarget() == null && !guard.getGuild().equals(mob.getGuild())) if (guard.getCombatTarget() == null && !guard.getGuild().equals(mob.getGuild()))
guard.setCombatTarget(mob); guard.setCombatTarget(mob);
if (mob.isSiege())
MovementManager.sendRWSSMsg(mob);
CombatManager.combatCycle(mob, target); CombatManager.combatCycle(mob, target);
if (mob.isSiege()) {
PowerProjectileMsg ppm = new PowerProjectileMsg(mob, target);
ppm.setRange(50);
DispatchManager.dispatchMsgToInterestArea(mob, ppm, DispatchChannel.SECONDARY, MBServerStatics.CHARACTER_LOAD_RANGE, false, false);
}
} catch (Exception e) { } catch (Exception e) {
Logger.info(mob.getObjectUUID() + " " + mob.getName() + " Failed At: AttackBuilding" + " " + e.getMessage()); Logger.error(e, mob.getObjectUUID() + " " + mob.getName() + " Failed At: AttackBuilding");
} }
} }
@@ -186,7 +181,7 @@ public class MobAI {
CombatManager.combatCycle(mob, target); CombatManager.combatCycle(mob, target);
} catch (Exception e) { } catch (Exception e) {
Logger.info(mob.getObjectUUID() + " " + mob.getName() + " Failed At: AttackMob" + " " + e.getMessage()); Logger.error(e, mob.getObjectUUID() + " " + mob.getName() + " Failed At: AttackMob");
} }
} }
@@ -248,7 +243,7 @@ public class MobAI {
MovementUtilities.aiMove(mob, mob.destination, true); MovementUtilities.aiMove(mob, mob.destination, true);
} catch (Exception e) { } catch (Exception e) {
Logger.info(mob.getObjectUUID() + " " + mob.getName() + " Failed At: AttackTarget" + " " + e.getMessage()); Logger.error(e, mob.getObjectUUID() + " " + mob.getName() + " Failed At: Patrol");
} }
} }
@@ -290,7 +285,7 @@ public class MobAI {
return mob.nextCastTime <= System.currentTimeMillis(); return mob.nextCastTime <= System.currentTimeMillis();
} catch (Exception e) { } catch (Exception e) {
Logger.info(mob.getObjectUUID() + " " + mob.getName() + " Failed At: canCast" + " " + e.getMessage()); Logger.error(e, mob.getObjectUUID() + " " + mob.getName() + " Failed At: canCast");
} }
return false; return false;
} }
@@ -387,7 +382,7 @@ public class MobAI {
return true; return true;
} }
} catch (Exception e) { } catch (Exception e) {
Logger.info(mob.getObjectUUID() + " " + mob.getName() + " Failed At: MobCast" + " " + e.getMessage()); Logger.error(e, mob.getObjectUUID() + " " + mob.getName() + " Failed At: MobCast");
} }
return false; return false;
} }
@@ -452,7 +447,7 @@ public class MobAI {
mob.nextCallForHelp = System.currentTimeMillis() + 60000; mob.nextCallForHelp = System.currentTimeMillis() + 60000;
} catch (Exception e) { } catch (Exception e) {
Logger.info(mob.getObjectUUID() + " " + mob.getName() + " Failed At: MobCallForHelp" + " " + e.getMessage()); Logger.error(e, mob.getObjectUUID() + " " + mob.getName() + " Failed At: MobCallForHelp");
} }
} }
@@ -518,7 +513,16 @@ public class MobAI {
boolean bypassLoadedPlayerCheck = false; boolean bypassLoadedPlayerCheck = false;
if (mob.isPlayerGuard() || mob.isSiege()) { if (mob.isPlayerGuard() || mob.isSiege()) {
bypassLoadedPlayerCheck = true; bypassLoadedPlayerCheck = true;
if (mob.combatTarget != null && mob.combatTarget.getObjectType().equals(mbEnums.GameObjectType.PlayerCharacter))
if (mob.isSiege()) {
// siege engines hold a character target until it dies or leaves their attack range
if (mob.combatTarget != null && AbstractCharacter.IsAbstractCharacter(mob.combatTarget))
if (mob.combatTarget.loc.distanceSquared(mob.loc) > mob.getRange() * mob.getRange())
mob.setCombatTarget(null);
} else if (mob.combatTarget != null && mob.combatTarget.getObjectType().equals(mbEnums.GameObjectType.PlayerCharacter))
if (mob.combatTarget.loc.distanceSquared(mob.loc) > 10000) if (mob.combatTarget.loc.distanceSquared(mob.loc) > 10000)
mob.setCombatTarget(null); mob.setCombatTarget(null);
} }
@@ -544,7 +548,7 @@ public class MobAI {
PlayerCharacter target = (PlayerCharacter) mob.getCombatTarget(); PlayerCharacter target = (PlayerCharacter) mob.getCombatTarget();
if (!mob.playerAgroMap.containsKey(target.getObjectUUID())) { if (!mob.isSiege() && !mob.playerAgroMap.containsKey(target.getObjectUUID())) {
mob.setCombatTarget(null); mob.setCombatTarget(null);
return; return;
} }
@@ -575,7 +579,7 @@ public class MobAI {
break; break;
} }
} catch (Exception e) { } catch (Exception e) {
Logger.info(mob.getObjectUUID() + " " + mob.getName() + " Failed At: DetermineAction" + " " + e.getMessage()); Logger.error(e, mob.getObjectUUID() + " " + mob.getName() + " Failed At: DetermineAction");
} }
} }
@@ -630,7 +634,7 @@ public class MobAI {
} }
} }
if (aiAgent.getCombatTarget() == null) { if (PET_AGGRO_ENABLED && aiAgent.getCombatTarget() == null) {
//look for pets to aggro if no players found to aggro //look for pets to aggro if no players found to aggro
@@ -649,7 +653,7 @@ public class MobAI {
} }
} }
} catch (Exception e) { } catch (Exception e) {
Logger.info(aiAgent.getObjectUUID() + " " + aiAgent.getName() + " Failed At: CheckForAggro" + " " + e.getMessage()); Logger.error(e, aiAgent.getObjectUUID() + " " + aiAgent.getName() + " Failed At: CheckForAggro");
} }
} }
@@ -702,7 +706,7 @@ public class MobAI {
} }
} }
} catch (Exception e) { } catch (Exception e) {
Logger.info(mob.getObjectUUID() + " " + mob.getName() + " Failed At: CheckMobMovement" + " " + e.getMessage()); Logger.error(e, mob.getObjectUUID() + " " + mob.getName() + " Failed At: CheckMobMovement");
} }
} }
@@ -746,7 +750,7 @@ public class MobAI {
ReSpawner.respawnQueue.put(aiAgent); ReSpawner.respawnQueue.put(aiAgent);
} }
} catch (Exception e) { } catch (Exception e) {
Logger.info(aiAgent.getObjectUUID() + " " + aiAgent.getName() + " Failed At: CheckForRespawn" + " " + e.getMessage()); Logger.error(e, aiAgent.getObjectUUID() + " " + aiAgent.getName() + " Failed At: CheckForRespawn");
} }
} }
@@ -761,7 +765,7 @@ public class MobAI {
if (mob.getCombatTarget() == null) if (mob.getCombatTarget() == null)
return; return;
if (mob.getCombatTarget().getObjectType().equals(mbEnums.GameObjectType.PlayerCharacter) && !MovementUtilities.inRangeDropAggro(mob, (PlayerCharacter) mob.getCombatTarget()) && !mob.agentType.equals(mbEnums.AIAgentType.PET)) { if (mob.getCombatTarget().getObjectType().equals(mbEnums.GameObjectType.PlayerCharacter) && !MovementUtilities.inRangeDropAggro(mob, (PlayerCharacter) mob.getCombatTarget()) && !mob.agentType.equals(mbEnums.AIAgentType.PET) && !mob.isSiege()) {
mob.setCombatTarget(null); mob.setCombatTarget(null);
return; return;
@@ -769,7 +773,7 @@ public class MobAI {
attackTarget(mob, mob.getCombatTarget()); attackTarget(mob, mob.getCombatTarget());
} catch (Exception e) { } catch (Exception e) {
Logger.info(mob.getObjectUUID() + " " + mob.getName() + " Failed At: CheckForAttack" + " " + e.getMessage()); Logger.error(e, mob.getObjectUUID() + " " + mob.getName() + " Failed At: CheckForAttack");
} }
} }
@@ -812,7 +816,7 @@ public class MobAI {
mob.playerAgroMap.replaceAll((e, v) -> 0f); mob.playerAgroMap.replaceAll((e, v) -> 0f);
} }
} catch (Exception e) { } catch (Exception e) {
Logger.info(mob.getObjectUUID() + " " + mob.getName() + " Failed At: CheckToSendMobHome" + " " + e.getMessage()); Logger.error(e, mob.getObjectUUID() + " " + mob.getName() + " Failed At: CheckToSendMobHome");
} }
} }
@@ -851,7 +855,7 @@ public class MobAI {
mob.updateMovementState(); mob.updateMovementState();
mob.updateLocation(); mob.updateLocation();
} catch (Exception e) { } catch (Exception e) {
Logger.info(mob.getObjectUUID() + " " + mob.getName() + " Failed At: chaseTarget" + " " + e.getMessage()); Logger.error(e, mob.getObjectUUID() + " " + mob.getName() + " Failed At: chaseTarget");
} }
} }
@@ -877,13 +881,17 @@ public class MobAI {
if (aggroMob.agentType.equals(mbEnums.AIAgentType.PET)) if (aggroMob.agentType.equals(mbEnums.AIAgentType.PET))
continue; continue;
//don't attack siege engines
if (aggroMob.isSiege())
continue;
if (mob.getLoc().distanceSquared2D(aggroMob.getLoc()) > sqr(50)) if (mob.getLoc().distanceSquared2D(aggroMob.getLoc()) > sqr(50))
continue; continue;
mob.setCombatTarget(aggroMob); mob.setCombatTarget(aggroMob);
} }
} catch (Exception e) { } catch (Exception e) {
Logger.info(mob.getObjectUUID() + " " + mob.getName() + " Failed At: SafeGuardAggro" + " " + e.getMessage()); Logger.error(e, mob.getObjectUUID() + " " + mob.getName() + " Failed At: SafeGuardAggro");
} }
} }
@@ -913,7 +921,7 @@ public class MobAI {
checkForAttack(mob); //only check to attack if combat target is not null checkForAttack(mob); //only check to attack if combat target is not null
} catch (Exception e) { } catch (Exception e) {
Logger.info(mob.getObjectUUID() + " " + mob.getName() + " Failed At: GuardLogic" + " " + e.getMessage()); Logger.error(e, mob.getObjectUUID() + " " + mob.getName() + " Failed At: GuardLogic");
} }
} }
@@ -946,7 +954,7 @@ public class MobAI {
mob.setHealth(mob.getHealthMax()); mob.setHealth(mob.getHealthMax());
} }
} catch (Exception e) { } catch (Exception e) {
Logger.info(mob.getObjectUUID() + " " + mob.getName() + " Failed At: PetLogic" + " " + e.getMessage()); Logger.error(e, mob.getObjectUUID() + " " + mob.getName() + " Failed At: PetLogic");
} }
} }
@@ -970,7 +978,7 @@ public class MobAI {
} }
checkForAttack(mob); checkForAttack(mob);
} catch (Exception e) { } catch (Exception e) {
Logger.info(mob.getObjectUUID() + " " + mob.getName() + " Failed At: HamletGuardLogic" + " " + e.getMessage()); Logger.error(e, mob.getObjectUUID() + " " + mob.getName() + " Failed At: HamletGuardLogic");
} }
} }
@@ -1043,7 +1051,7 @@ public class MobAI {
checkForAttack(mob); checkForAttack(mob);
} catch (Exception e) { } catch (Exception e) {
Logger.info(mob.getObjectUUID() + " " + mob.getName() + " Failed At: DefaultLogic" + " " + e.getMessage()); Logger.error(e, mob.getObjectUUID() + " " + mob.getName() + " Failed At: DefaultLogic");
} }
} }
@@ -1099,25 +1107,11 @@ public class MobAI {
return; return;
} }
} }
if (mob.getCombatTarget() == null) {
//look for siege equipment to aggro if no players found to aggro // guards never auto-aggro siege engines; they only respond when one damages a city building
HashSet<AbstractWorldObject> awoList = WorldGrid.getObjectsInRangePartial(mob, MobAIThread.AI_BASE_AGGRO_RANGE, MBServerStatics.MASK_SIEGE);
for (AbstractWorldObject awoMob : awoList) {
Mob aggroMob = (Mob) awoMob;
if (guardCanAggro(mob, aggroMob)) {
mob.setCombatTarget(aggroMob);
return;
}
}
}
} catch (Exception e) { } catch (Exception e) {
Logger.info(mob.getObjectUUID() + " " + mob.getName() + " Failed At: CheckForPlayerGuardAggro" + e.getMessage()); Logger.error(e, mob.getObjectUUID() + " " + mob.getName() + " Failed At: CheckForPlayerGuardAggro");
} }
} }
@@ -1180,7 +1174,7 @@ public class MobAI {
} }
} }
} catch (Exception e) { } catch (Exception e) {
Logger.info(mob.getObjectUUID() + " " + mob.getName() + " Failed At: GuardCanAggro" + " " + e.getMessage()); Logger.error(e, mob.getObjectUUID() + " " + mob.getName() + " Failed At: GuardCanAggro");
} }
return false; return false;
} }
@@ -1232,7 +1226,7 @@ public class MobAI {
} }
} }
} catch (Exception e) { } catch (Exception e) {
Logger.info(mob.getObjectUUID() + " " + mob.getName() + " Failed At: randomGuardPatrolPoints" + " " + e); Logger.error(e, mob.getObjectUUID() + " " + mob.getName() + " Failed At: randomGuardPatrolPoints");
} }
} }
@@ -1267,7 +1261,7 @@ public class MobAI {
} }
return mostHatedTarget; return mostHatedTarget;
} catch (Exception e) { } catch (Exception e) {
Logger.info(mob.getObjectUUID() + " " + mob.getName() + " Failed At: ChangeTargetFromMostHated" + " " + e.getMessage()); Logger.error(e, mob.getObjectUUID() + " " + mob.getName() + " Failed At: ChangeTargetFromMostHated");
} }
return null; return null;
} }
+88 -8
View File
@@ -656,12 +656,30 @@ public abstract class AbstractCharacter extends AbstractWorldObject {
AssignDamageAtrForPlayers(abstractCharacter, equipped.get(EquipSlotType.LHELD), false, equipped.get(EquipSlotType.LHELD)); AssignDamageAtrForPlayers(abstractCharacter, equipped.get(EquipSlotType.LHELD), false, equipped.get(EquipSlotType.LHELD));
if (abstractCharacter.getObjectType().equals(GameObjectType.Mob)) { if (abstractCharacter.getObjectType().equals(GameObjectType.Mob)) {
Mob mob = (Mob) abstractCharacter; Mob mob = (Mob) abstractCharacter;
abstractCharacter.minDamageHandOne = (int) mob.mobBase.getDamageMin(); ItemTemplate rune = ItemTemplate.templates.get(mob.loadID);
abstractCharacter.minDamageHandTwo = (int) mob.mobBase.getDamageMin(); float minDamage = mob.mobBase.getDamageMin();
abstractCharacter.maxDamageHandOne = (int) mob.mobBase.getDamageMax(); float maxDamage = mob.mobBase.getDamageMax();
abstractCharacter.maxDamageHandTwo = (int) mob.mobBase.getDamageMax(); int attackRating = mob.mobBase.getAttackRating();
abstractCharacter.atrHandOne = mob.mobBase.getAttackRating();
abstractCharacter.atrHandTwo = mob.mobBase.getAttackRating(); // The client (ArcMobile, 0x46D200) takes mob damage straight from the rune MINDAMAGE/MAXDAMAGE,
// with no stat scaling. Use the rune when it has damage, otherwise keep the mobbase value.
if (rune != null && rune.rune_max_damage > 0) {
minDamage = rune.rune_min_damage;
maxDamage = rune.rune_max_damage;
}
// Likewise ATR comes straight from the rune ATTACK for mobiles (0x466570).
if (rune != null && rune.rune_attack > 0)
attackRating = rune.rune_attack;
abstractCharacter.minDamageHandOne = (int) minDamage;
abstractCharacter.minDamageHandTwo = (int) minDamage;
abstractCharacter.maxDamageHandOne = (int) maxDamage;
abstractCharacter.maxDamageHandTwo = (int) maxDamage;
abstractCharacter.atrHandOne = attackRating;
abstractCharacter.atrHandTwo = attackRating;
abstractCharacter.defenseRating = mob.mobBase.getDefenseRating(); abstractCharacter.defenseRating = mob.mobBase.getDefenseRating();
} }
} }
@@ -717,9 +735,18 @@ public abstract class AbstractCharacter extends AbstractWorldObject {
mastDam = CharacterSkill.getQuickMastery(abstractCharacter, "Unarmed Combat Mastery"); mastDam = CharacterSkill.getQuickMastery(abstractCharacter, "Unarmed Combat Mastery");
else else
mastDam = masteryPercentage; mastDam = masteryPercentage;
// TODO Correct these // Base fist damage from the race rune natural attack (1-4 for all races), scaled below like a weapon.
// Matches the client's unarmed damage calc (0x46D200).
float[] naturalAttack = CombatManager.getNaturalAttack(abstractCharacter, mainHand ? EquipSlotType.RHELD : EquipSlotType.LHELD);
if (naturalAttack != null && naturalAttack.length > 9) {
min = (int) naturalAttack[8];
max = (int) naturalAttack[9];
} else {
min = 1; min = 1;
max = 3; max = 3;
}
} else { } else {
if (mainHand) if (mainHand)
@@ -1476,13 +1503,22 @@ public abstract class AbstractCharacter extends AbstractWorldObject {
Mob mob = (Mob) this; Mob mob = (Mob) this;
if (mob.isSiege()) { if (mob.isSiege()) {
return 300; return CombatManager.getSiegeRange(mob);
} }
float range = 8; float range = 8;
if ((this).charItemManager.equipped.get(EquipSlotType.RHELD) != null) { if ((this).charItemManager.equipped.get(EquipSlotType.RHELD) != null) {
range = ((Mob) this).charItemManager.equipped.get(EquipSlotType.RHELD).template.item_weapon_max_range; range = ((Mob) this).charItemManager.equipped.get(EquipSlotType.RHELD).template.item_weapon_max_range;
} else if (((Mob) this).charItemManager.equipped.get(EquipSlotType.LHELD) != null) { } else if (((Mob) this).charItemManager.equipped.get(EquipSlotType.LHELD) != null) {
range = ((Mob) this).charItemManager.equipped.get(EquipSlotType.LHELD).template.item_weapon_max_range; range = ((Mob) this).charItemManager.equipped.get(EquipSlotType.LHELD).template.item_weapon_max_range;
} else {
// Unarmed: use the rune natural attack range when it has one (e.g. Gasser 150), not clamped.
// Most melee runes have 0, so they keep the default of 8.
float naturalRange = CombatManager.getNaturalAttackRange(mob, EquipSlotType.RHELD);
if (naturalRange > 0)
return naturalRange;
} }
// TODO Is this clamp from live? // TODO Is this clamp from live?
@@ -1827,6 +1863,15 @@ public abstract class AbstractCharacter extends AbstractWorldObject {
if (this.getObjectType().equals(GameObjectType.Mob)) { if (this.getObjectType().equals(GameObjectType.Mob)) {
//handle hate value addition //handle hate value addition
Mob target = (Mob) this; Mob target = (Mob) this;
// Siege engines switch to whoever damages them, players and pets alike.
// Misses and non-damaging powers do not pull them.
if (target.isSiege() && value < 0f && attacker != null && !attacker.equals(target)) {
target.setCombatTarget(attacker);
flagSiegeAttackerHostile(target, attacker);
}
if (attacker.getObjectType().equals(GameObjectType.PlayerCharacter)) { if (attacker.getObjectType().equals(GameObjectType.PlayerCharacter)) {
if (target.playerAgroMap.containsKey(attacker.getObjectUUID())) if (target.playerAgroMap.containsKey(attacker.getObjectUUID()))
@@ -1860,6 +1905,41 @@ public abstract class AbstractCharacter extends AbstractWorldObject {
return 0; return 0;
} }
private static void flagSiegeAttackerHostile(Mob siegeEngine, AbstractCharacter attacker) {
// Damaging a siege engine on a city grid flags the player hostile to that
// city, unless they belong to the city's nation or an allied nation.
// Pets flag their owner.
City city;
AbstractCharacter flagged;
Guild cityNation;
city = ZoneManager.getCityAtLocation(siegeEngine.loc);
if (city == null)
return;
flagged = attacker;
if (attacker.getObjectType().equals(GameObjectType.Mob))
flagged = ((Mob) attacker).guardCaptain;
if (flagged == null || !flagged.getObjectType().equals(GameObjectType.PlayerCharacter))
return;
cityNation = city.getGuild().getNation();
if (flagged.getGuild().getNation().equals(cityNation))
return;
if (cityNation.getAllyList().contains(flagged.getGuild().getNation()))
return;
if (!city.cityOutlaws.contains(flagged.getObjectUUID()))
city.cityOutlaws.add(flagged.getObjectUUID());
}
public float getCurrentHitpoints() { public float getCurrentHitpoints() {
return this.health.get(); return this.health.get();
} }
@@ -148,9 +148,20 @@ public abstract class AbstractIntelligenceAgent extends AbstractCharacter {
public void dismiss() { public void dismiss() {
boolean isSiegeEngine;
isSiegeEngine = this.getObjectType() == GameObjectType.Mob && ((Mob) this).isSiege();
if (this.isPet()) { if (this.isPet()) {
if ((this.agentType.equals(mbEnums.AIAgentType.PET))) { //delete summoned pet if (isSiegeEngine) {
// Released siege engines stay in place, alive, and keep firing at
// their current target until a Rod of Command takes them again.
this.agentType = mbEnums.AIAgentType.MOBILE;
} else if ((this.agentType.equals(mbEnums.AIAgentType.PET))) { //delete summoned pet
WorldGrid.RemoveWorldObject(this); WorldGrid.RemoveWorldObject(this);
@@ -167,6 +178,7 @@ public abstract class AbstractIntelligenceAgent extends AbstractCharacter {
// clear owner and set not alive // clear owner and set not alive
if (!isSiegeEngine)
this.isAlive.set(false); this.isAlive.set(false);
PlayerCharacter owner = (PlayerCharacter) this.guardCaptain; PlayerCharacter owner = (PlayerCharacter) this.guardCaptain;
@@ -188,6 +200,8 @@ public abstract class AbstractIntelligenceAgent extends AbstractCharacter {
this.guardCaptain = null; this.guardCaptain = null;
} }
if (isSiegeEngine && this.isAlive())
WorldGrid.updateObject(this);
} }
} }
+3 -1
View File
@@ -395,8 +395,10 @@ public class Building extends AbstractWorldObject {
//friendly building has been attacked, add attacker to city outlaw list //friendly building has been attacked, add attacker to city outlaw list
if (!playerCity.cityOutlaws.contains(attacker.getObjectUUID()) && attacker.getObjectType().equals(GameObjectType.PlayerCharacter)) if (!playerCity.cityOutlaws.contains(attacker.getObjectUUID()) && attacker.getObjectType().equals(GameObjectType.PlayerCharacter))
playerCity.cityOutlaws.add(attacker.getObjectUUID()); playerCity.cityOutlaws.add(attacker.getObjectUUID());
//only city guards respond; siege engines and pets in the zone are left alone
for (Mob guard : playerCity.getParent().zoneMobSet) for (Mob guard : playerCity.getParent().zoneMobSet)
if (guard.combatTarget == null) if (guard.isPlayerGuard() && guard.combatTarget == null)
guard.setCombatTarget(attacker); guard.setCombatTarget(attacker);
} }
} }
+20
View File
@@ -138,6 +138,11 @@ public class ItemTemplate {
public final EnumSet<mbEnums.MobBehaviourType> rune_group_role_set = EnumSet.noneOf(mbEnums.MobBehaviourType.class); public final EnumSet<mbEnums.MobBehaviourType> rune_group_role_set = EnumSet.noneOf(mbEnums.MobBehaviourType.class);
public boolean rune_renderable = false; public boolean rune_renderable = false;
public int rune_natural_power_attack; public int rune_natural_power_attack;
// One float[] per rune_naturalattacks entry. Same "attack info" struct as a weapon (verified in IDA, see
// MBDATA/docs/client-map.md): [0] attack speed (WEPSPEED), [1] range (MAXRANGE), [2] unknown int,
// [3] equip slot (1 = RHELD, 2 = LHELD), [4] projectile template id, [5] projectile speed, [6] swing anim,
// [7] damage type, [8] min damage, [9] max damage
public final ArrayList<float[]> rune_natural_attacks = new ArrayList<>();
public final HashMap<String, String> rune_sparse_data = new HashMap<>(); public final HashMap<String, String> rune_sparse_data = new HashMap<>();
public final HashMap<String, int[]> rune_skill_adj = new HashMap<>(); public final HashMap<String, int[]> rune_skill_adj = new HashMap<>();
public final EnumSet<mbEnums.MonsterType> rune_enemy_monster_types = EnumSet.noneOf(mbEnums.MonsterType.class); public final EnumSet<mbEnums.MonsterType> rune_enemy_monster_types = EnumSet.noneOf(mbEnums.MonsterType.class);
@@ -554,6 +559,21 @@ public class ItemTemplate {
rune_renderable = jsonObject.getBoolean("rune_renderable"); rune_renderable = jsonObject.getBoolean("rune_renderable");
rune_natural_power_attack = jsonObject.getInt("rune_natural_power_attack"); rune_natural_power_attack = jsonObject.getInt("rune_natural_power_attack");
if (jsonObject.has("rune_naturalattacks")) {
JSONArray natural_attacks_json = jsonObject.getJSONArray("rune_naturalattacks");
for (Object attackEntry : natural_attacks_json) {
JSONArray attackArray = (JSONArray) attackEntry;
float[] attack = new float[attackArray.length()];
for (int i = 0; i < attackArray.length(); i++)
attack[i] = attackArray.getFloat(i);
rune_natural_attacks.add(attack);
}
}
JSONObject rune_sparse_json = jsonObject.getJSONObject("rune_sparse_data"); JSONObject rune_sparse_json = jsonObject.getJSONObject("rune_sparse_data");
for (String sparseType : rune_sparse_json.keySet()) { for (String sparseType : rune_sparse_json.keySet()) {
+11
View File
@@ -552,6 +552,12 @@ public class Mob extends AbstractIntelligenceAgent implements Delayed {
petMinion.despawned = false; petMinion.despawned = false;
petMinion.runAfterLoad(); petMinion.runAfterLoad();
// Summoned pets carry MASK_PET. Necro pets keep their own NecroPet handling.
if (!petMinion.isNecroPet())
petMinion.setObjectTypeMask(petMinion.getObjectTypeMask() | MBServerStatics.MASK_PET);
DbManager.addToCache(petMinion); DbManager.addToCache(petMinion);
petMinion.setLoc(petMinion.bindLoc); petMinion.setLoc(petMinion.bindLoc);
@@ -1318,6 +1324,11 @@ public class Mob extends AbstractIntelligenceAgent implements Delayed {
if (!ac.getObjectType().equals(GameObjectType.PlayerCharacter)) if (!ac.getObjectType().equals(GameObjectType.PlayerCharacter))
return; return;
// siege engines only switch targets on damage, see AbstractCharacter.modifyHealth
if (this.isSiege())
return;
if (this.getCombatTarget() == null) if (this.getCombatTarget() == null)
this.setCombatTarget(ac); this.setCombatTarget(ac);
+2 -2
View File
@@ -4184,8 +4184,9 @@ public class PlayerCharacter extends AbstractCharacter {
} else if (currentPet.isSiege()) { } else if (currentPet.isSiege()) {
currentPet.agentType = AIAgentType.MOBILE; currentPet.agentType = AIAgentType.MOBILE;
// released siege engine keeps its current target
currentPet.guardCaptain = null; currentPet.guardCaptain = null;
currentPet.setCombatTarget(null);
if (currentPet.isAlive()) if (currentPet.isAlive())
WorldGrid.updateObject(currentPet); WorldGrid.updateObject(currentPet);
} }
@@ -4193,7 +4194,6 @@ public class PlayerCharacter extends AbstractCharacter {
toCommand.setPet(this, false); toCommand.setPet(this, false);
this.setPet(toCommand); this.setPet(toCommand);
toCommand.setCombatTarget(null);
PetMsg petMsg = new PetMsg(6, toCommand); PetMsg petMsg = new PetMsg(6, toCommand);
Dispatch dispatch = Dispatch.borrow(this, petMsg); Dispatch dispatch = Dispatch.borrow(this, petMsg);
DispatchManager.dispatchMsgDispatch(dispatch, DispatchChannel.PRIMARY); DispatchManager.dispatchMsgDispatch(dispatch, DispatchChannel.PRIMARY);
@@ -246,7 +246,9 @@ public class ApplyEffectPowerAction extends AbstractPowerAction {
if (this.effectID.equals("TAUNT")) { if (this.effectID.equals("TAUNT")) {
if (awo != null && awo.getObjectType() == GameObjectType.Mob) { // siege engines only switch targets on damage, see AbstractCharacter.modifyHealth
if (awo != null && awo.getObjectType() == GameObjectType.Mob && !((Mob) awo).isSiege()) {
((Mob) awo).setCombatTarget(source); ((Mob) awo).setCombatTarget(source);
ChatSystemMsg msg = ChatManager.CombatInfo(source, awo); ChatSystemMsg msg = ChatManager.CombatInfo(source, awo);
DispatchManager.sendToAllInRange(source, msg); DispatchManager.sendToAllInRange(source, msg);