Hey Whiskey, I too, your suggestions along with some other ideas, some I was already working on before I got to look at your code, but I will try and post what I have done. Right now I have given each class a threat multiplier. You can tweak this as one sees fit and from testing.
A group of two, war and a mage go into a fight. After a few rounds the mage cast a high level spell and suddenly takes the lead in the hate table. The warrior will probably have to do a "taunt" or "shout" of some type to regain the hate or possibly a 'rescue" will work by elevating the warriors number on the aggro table of the mob. The hard part I had was when a mage did an AOE spell with 4 mobs in the room. I finally got the warrior to get on each mobs aggro table and leap above the mag in terms of hate.
Here is some of the code.
Code:
/**
* @author Soth
* @date 2026.03.10
* @brief Modify threat for group members
* @details Updates the aggro table for the mob.
* @param victim: the mob being hit or seeing a heal
* @param ch: the player doing the action
* @param amount: the raw damage or healing value
*/
void modify_threat(struct char_data *victim, struct char_data *ch, int amount)
{
struct aggro_node *node;
float multiplier = 1.0;
/* Safety checks: Only NPCs have aggro tables, and only players go on them. */
if (!victim || !ch || !IS_NPC(victim) || IS_NPC(ch))
return;
/* Class Multipliers: Help define the 'Roles' in your group */
switch (GET_CLASS(ch)) {
case CLASS_WARRIOR:
multiplier = 1.5; /* Warriors generate slightly more hate to act as tanks */
if (AFF_FLAGGED(ch, AFF_VANGUARD)) {
multiplier = 2.1; // 50% increase in threat generation while in Vanguard
}
break;
case CLASS_CLERIC:
multiplier = 0.9; /* Healers generate slightly less hate for balance */
break;
case CLASS_MAGIC_USER:
multiplier = 0.8; /* Standard threat for dps */
break;
case CLASS_NECROMANCER:
multiplier = 1.0; /* Standard threat for dps */
break;
case CLASS_THIEF:
multiplier = 1.0; /* Standard threat for dps */
break;
case CLASS_MONK:
multiplier = 1.0; /* Standard threat for dps */
break;
default:
multiplier = 1.0; /* Standard threat for dps */
break;
}
int total_threat = (int)(amount * multiplier);
/* Search for the player on the mob's table */
for (node = victim->mob_specials.aggro_table; node; node = node->next) {
if (node->ch == ch) {
node->amount += total_threat;
return;
}
}
/* If they aren't on the table yet, add a new node at the head */
CREATE(node, struct aggro_node, 1);
node->ch = ch;
node->amount = total_threat;
node->next = victim->mob_specials.aggro_table;
victim->mob_specials.aggro_table = node;
}
/**
* @author
* @date
* @brief
*/
int damage(struct char_data *ch, struct char_data *victim, int dam, int attacktype)
{
//log("DEBUG: damage() called. Attacker: %s, Victim: %s, Skill: %d", ch ? GET_NAME(ch) : "NONE", GET_NAME(victim), attacktype);
if (!validate_damage(ch, victim, &dam))
return 0;
/* NEW LOGIC: If the attacker is NULL OR the attacker is the victim (self-damage) */
/* and they are currently in a fight, give credit to the opponent. */
if ((ch == NULL || ch == victim) && FIGHTING(victim) != NULL) {
ch = FIGHTING(victim);
}
setup_combat(ch, victim);
//log("DEBUG: setup_combat complete. Victim fighting: %s", FIGHTING(victim) ? GET_NAME(FIGHTING(victim)) : "NONE");
dam = apply_attacker_modifiers(ch, victim, dam);
check_death_blow(ch, victim);
dam = apply_damage_mitigation(ch, victim, dam);
/* Vanguard stance for a warrior */
if (AFF_FLAGGED(ch, AFF_VANGUARD)) {
dam = (int)(dam * 0.80); // Reduce damage by 20%
}
/* Apply the damage to HP */
GET_HIT(victim) -= dam;
/* Inside damage() function in fight.c */
if (victim != ch) {
int threat_amt = dam;
if (attacktype == 170) { // HARDCODE 170 to be sure
threat_amt = 15000;
//log("SHOUT_LOG: SUCCESS! Applied 15,000 threat to %s", GET_NAME(victim));
} else {
// Normal combat threat
if (AFF_FLAGGED(ch, AFF_VANGUARD)) {
threat_amt = dam * 10;
}
}
if (threat_amt > 0 || attacktype == 170) {
modify_threat(victim, ch, threat_amt);
}
}
/* Only gain swing-by-swing XP if we have a valid attacker */
if (ch && ch != victim)
gain_exp(ch, dam / 2, NULL, TRUE);
update_pos(victim);
send_damage_output(ch, victim, dam, attacktype);
handle_victim_state(ch, victim);
if (GET_POS(victim) == POS_DEAD) {
//log("DEBUG: damage() detected POS_DEAD. ch is: %s", ch ? GET_NAME(ch) : "NULL");
return handle_death(ch, victim);
}
return dam;
}
/**
* @author Soth
* @date 2026.03.09
* @brief Gets threat from players
*/
long get_threat(struct char_data *mob, struct char_data *target) {
struct aggro_node *node;
if (!mob || !target || !IS_NPC(mob)) return 0;
for (node = mob->mob_specials.aggro_table; node; node = node->next) {
if (node->ch == target)
return node->amount;
}
return 0;
}
/* Control the fight
* This is called every 2 seconds from <comm.c>*/
/**
* @date 2026.03.06
* @brief Performs the violence
*/
#define VALID_FIGHTER(ch) (FIGHTING(ch) && IN_ROOM(ch) == IN_ROOM(FIGHTING(ch)))
void perform_violence(void)
{
struct char_data *ch, *tch, *next_combat_list;
struct aggro_node *node, *top_node = NULL;
long max_threat = -1;
float switch_threshold = 1.1;
for (ch = combat_list; ch; ch = next_combat_list) {
next_combat_list = ch->next_fighting;
max_threat = -1;
top_node = NULL;
/* only NPCs use the aggro table to switch targets */
if (IS_NPC(ch) && FIGHTING(ch)) {
for (node = ch->mob_specials.aggro_table; node; node = node->next) {
if (node->ch && IN_ROOM(node->ch) == IN_ROOM(ch) && GET_POS(node->ch) > POS_DEAD) {
if (node->amount > max_threat) {
max_threat = node->amount;
top_node = node;
}
}
}
if (top_node && FIGHTING(ch) && top_node->ch != FIGHTING(ch)) {
long current_target_threat = get_threat(ch, FIGHTING(ch));
switch_threshold = 1.1;
if (AFF_FLAGGED(FIGHTING(ch), AFF_VANGUARD)) {
switch_threshold = 1.5;
}
if (max_threat > current_target_threat && max_threat <= (current_target_threat * switch_threshold)) {
if (rand_number(0, 2) == 0) {
act("\tO$n\tn glares menancingly at \tO$N\tn, looking like $e might switch targets!", FALSE, ch, 0, top_node->ch, TO_NOTVICT);
act("\tO$n\tn glares menancingly at YOU, looking like $e might switch targets!", FALSE, ch, 0, top_node->ch, TO_VICT);
}
}
if (max_threat > (current_target_threat * switch_threshold)) {
act("\tO$n \tRscreams in a blind rage and turns toward \tO$N!\tn", FALSE, ch, 0, top_node->ch, TO_NOTVICT);
act("\tO$n\tR snarls and turns their eyes on YOU!\tn", FALSE, ch, 0, top_node->ch, TO_VICT);
stop_fighting(ch);
set_fighting(ch, top_node->ch);
}
}
}
/* Skip if not fighting or not in same room */
if (!FIGHTING(ch) || IN_ROOM(ch) != IN_ROOM(FIGHTING(ch))) {
/* --- NEW VANGUARD INTERCEPT LOGIC --- */
/* If we aren't fighting, but we are a Vanguard in a group, look for trouble */
if (!IS_NPC(ch) && AFF_FLAGGED(ch, AFF_VANGUARD) && GROUP(ch)) {
struct char_data *vict;
for (vict = world[IN_ROOM(ch)].people; vict; vict = vict->next_in_room) {
if (vict != ch && GROUP(vict) == GROUP(ch) && FIGHTING(vict) && IS_NPC(FIGHTING(vict))) {
act("@WYou see $N under attack and leap to intercept!@n", FALSE, ch, 0, vict, TO_CHAR);
act("@W$n leaps to intercept the attack on $N!@n", FALSE, ch, 0, vict, TO_NOTVICT);
set_fighting(ch, FIGHTING(vict));
break;
}
}
}
/* ------------------------------------ */
if (!FIGHTING(ch)) {
stop_fighting(ch);
continue;
}
}
/* Mob wait handling */
if (IS_NPC(ch) && GET_MOB_WAIT(ch) > 0) {
GET_MOB_WAIT(ch) -= PULSE_VIOLENCE;
continue;
}
if (IS_NPC(ch))
GET_MOB_WAIT(ch) = 0;
/* Ensure fighting position */
if (GET_POS(ch) < POS_FIGHTING) {
if (IS_NPC(ch)) {
GET_POS(ch) = POS_FIGHTING;
act("$n scrambles to $s feet!", TRUE, ch, 0, 0, TO_ROOM);
} else {
send_to_char(ch, "You can't fight while sitting!!\r\n");
continue;
}
}
/* Group auto-assist (standard) */
if (GROUP(ch) && GROUP(ch)->members && GROUP(ch)->members->iSize) {
struct iterator_data Iterator;
tch = (struct char_data *)merge_iterator(&Iterator, GROUP(ch)->members);
for (; tch; tch = next_in_list(&Iterator)) {
if (tch == ch || (!IS_NPC(tch) && !PRF_FLAGGED(tch, PRF_AUTOASSIST)))
continue;
if (IN_ROOM(ch) != IN_ROOM(tch) || FIGHTING(tch) || GET_POS(tch) != POS_STANDING)
continue;
if (!CAN_SEE(tch, ch))
continue;
do_assist(tch, GET_NAME(ch), 0, 0);
}
}
/* MAIN HIT */
if (FIGHTING(ch))
hit(ch, FIGHTING(ch), TYPE_UNDEFINED, 0.5);
if (!FIGHTING(ch))
continue;
/* Contagion, multi-attacks, and spec-procs follow... */
/* Contagion effect */
if (IS_NPC(ch) && AFF_FLAGGED(ch, AFF_CONTAGION)) {
if (rand_number(1, 100) > 50) {
damage(ch, ch, rand_number(1, GET_LEVEL(ch) ), SPELL_POISON);
if (!FIGHTING(ch))
continue;
}
}
/* Mob extra attacks */
if (IS_NPC(ch) && GET_LEVEL(ch) > 5) {
for (int i = 0; i < 2; i++) {
if (!FIGHTING(ch) || IN_ROOM(ch) != IN_ROOM(FIGHTING(ch))) {
stop_fighting(ch);
break;
}
if (MOB_FLAGGED(ch, MOB_BERSERKER)) {
if (rand_number(1, GET_LEVEL(ch)) < (GET_LEVEL(ch) / 2)) {
hit(ch, FIGHTING(ch), TYPE_UNDEFINED, 0.5);
if (!FIGHTING(ch))
break;
hit(ch, FIGHTING(ch), TYPE_UNDEFINED, 0.5);
if (!FIGHTING(ch))
break;
}
} else {
if (rand_number(1, GET_LEVEL(ch)) < (GET_LEVEL(ch) / 3)) {
hit(ch, FIGHTING(ch), TYPE_UNDEFINED, 0.5);
if (!FIGHTING(ch))
break;
}
}
}
}
/* Player multi-attacks */
if (!IS_NPC(ch) && FIGHTING(ch)) {
/* Off-hand attack */
if (GET_SKILL(ch, SKILL_DUAL_WIELD) > 0) {
if (FIGHTING(ch) && rand_number(1, 100) <= GET_SKILL(ch, SKILL_DUAL_WIELD) / 3) {
const char *attacker_msgs[] = {
"You sneak in a quick slash with your off-hand!",
"Your off-hand weapon arcs in a deadly strike!",
"With a deft flick, your off-hand attacks!"
};
const char *victim_msgs[] = {
"$n sneaks in a quick slash with $s off-hand weapon!",
"$n's off-hand weapon arcs in a deadly strike toward you!",
"With a deft flick, $n attacks you with $s off-hand weapon!"
};
const char *room_msgs[] = {
"$n sneaks in a quick slash with $s off-hand weapon!",
"$n's off-hand weapon arcs in a deadly strike!",
"With a deft flick, $n attacks with $s off-hand weapon!"
};
int idx = rand_number(0, 2);
send_to_char(ch, "\tY%s\tn\r\n", attacker_msgs[idx]);
act(victim_msgs[idx], FALSE, ch, NULL, FIGHTING(ch), TO_VICT);
act(room_msgs[idx], TRUE, ch, NULL, FIGHTING(ch), TO_NOTVICT);
hit(ch, FIGHTING(ch), TYPE_UNDEFINED, 0.5);
if (!FIGHTING(ch))
continue;
}
}
/* Second & third attack */
if (FIGHTING(ch) && rand_number(1, 100) < (GET_SKILL(ch, SKILL_ATTACK2) / 1.5)) {
hit(ch, FIGHTING(ch), TYPE_UNDEFINED, 0.5);
if (!FIGHTING(ch))
continue;
}
if (FIGHTING(ch) && rand_number(1, 100) < (GET_SKILL(ch, SKILL_ATTACK3) / 2)) {
hit(ch, FIGHTING(ch), TYPE_UNDEFINED, 0.5);
if (!FIGHTING(ch))
continue;
}
/* Player rage effect */
if (AFF_FLAGGED(ch, AFF_RAGE) && FIGHTING(ch)) {
if (GET_MOVE(ch) < 25) {
send_to_char(ch, "\tRYou feel too exhausted right now.\tn\r\n");
} else {
if (FIGHTING(ch)) {
send_to_char(ch, "\tRYou start to \tWfoam at the mouth\tR!!!!!\tn\r\n");
for (int i = 0; i < GET_LEVEL(ch) / 6; i++) {
if (!FIGHTING(ch))
break;
hit(ch, FIGHTING(ch), TYPE_UNDEFINED, 0.25);
GET_MOVE(ch) -= (GET_LEVEL(ch) / 4);
}
}
}
}
}
/* Mob spec proc */
if (MOB_FLAGGED(ch, MOB_SPEC) && GET_MOB_SPEC(ch) && !MOB_FLAGGED(ch, MOB_NOTDEADYET)) {
char actbuf[MAX_INPUT_LENGTH] = "";
(GET_MOB_SPEC(ch))(ch, ch, 0, actbuf);
}
}
}
Code:
/**
* @author Soth
* @date 2026.03.12
* @brief Challenging Shout
*/
ACMD(do_challenging_shout) {
struct char_data *vict, *next_vict;
struct aggro_node *node, *tank_node;
long current_max;
act("You let out a blood-curdling Challenging Shout!", FALSE, ch, 0, 0, TO_CHAR);
act("$n lets out a blood-curdling Challenging Shout!", FALSE, ch, 0, 0, TO_ROOM);
for (vict = world[IN_ROOM(ch)].people; vict; vict = next_vict) {
next_vict = vict->next_in_room;
if (IS_NPC(vict) && vict != ch) {
current_max = 0;
tank_node = NULL;
/* 1. Find current max AND check if the Tank (ch) is already on the table */
for (node = vict->mob_specials.aggro_table; node; node = node->next) {
if (node->amount > current_max)
current_max = node->amount;
if (node->ch == ch)
tank_node = node;
}
/* 2. Calculate the target value (1.6x to clear the 1.1x/1.5x threshold) */
long taunt_value = (long)(current_max * 1.6) + 1000;
/* 3. Manually update or create the node */
if (tank_node) {
tank_node->amount = taunt_value;
}
else {
/* Tank isn't on the table yet, we must create a new node */
CREATE(node, struct aggro_node, 1);
node->ch = ch;
node->amount = taunt_value;
node->next = vict->mob_specials.aggro_table;
vict->mob_specials.aggro_table = node;
}
}
}
if (found > 0) {
set_cooldown(ch, CD_CHALLENGING_SHOUT, 8);
WAIT_STATE(ch, PULSE_VIOLENCE);
}
else {
send_to_char(ch, "No enemies were close enough to hear your challenge.\r\n");
}
}
/**
* @author Soth
* @date 2026.03.09
* @brief Taunts the mob
*/
ACMD(do_taunt)
{
struct char_data *victim;
struct aggro_node *node;
char arg[MAX_INPUT_LENGTH]; /* FIX: Declare the missing 'arg' */
long max_threat = 0;
bool found = FALSE;
one_argument(argument, arg);
if (!(victim = get_char_vis(ch, arg, NULL, FIND_CHAR_ROOM))) {
if (FIGHTING(ch)) {
victim = FIGHTING(ch);
} else {
send_to_char(ch, "Taunt who?\r\n");
return;
}
}
if (victim == ch || !IS_NPC(victim)) {
send_to_char(ch, "You can't really taunt that.\r\n");
return;
}
if (GET_MOVE(ch) < 15) {
send_to_char(ch, "You are too winded to shout a challenge!\r\n");
return;
}
GET_MOVE(ch) -= 15;
/* Find the highest threat currently on the mob's table */
for (node = victim->mob_specials.aggro_table; node; node = node->next) {
if (node->amount > max_threat)
max_threat = node->amount;
}
/* Ensure the floor is at least 100 so the taunt does something on a fresh pull */
if (max_threat < 100) max_threat = 100;
/* FIX: Instead of set_threat, we find the Warrior and set their amount */
for (node = victim->mob_specials.aggro_table; node; node = node->next) {
if (node->ch == ch) {
node->amount = (long)(max_threat * 1.15);
found = TRUE;
break;
}
}
/* If the Warrior wasn't on the table yet, add them now */
if (!found) {
modify_threat(victim, ch, (int)(max_threat * 1.15));
}
act("You shout insults at $N, grabbing their attention!", FALSE, ch, 0, victim, TO_CHAR);
act("$n shouts insults at $N!", FALSE, ch, 0, victim, TO_NOTVICT);
act("$n shouts foul insults at YOU!", FALSE, ch, 0, victim, TO_VICT);
if (FIGHTING(victim) != ch) {
stop_fighting(victim);
set_fighting(victim, ch);
}
WAIT_STATE(ch, PULSE_VIOLENCE * 2);
}
/**
* @author Soth
* @date 2026.03.09
* @brief Vanguard Stance for Warrior
*/
ACMD(do_vanguard)
{
if (IS_NPC(ch)) return;
/* 1. Check if they even know the skill */
if (IS_NPC(ch) || !GET_SKILL(ch, SKILL_VANGUARD)) {
send_to_char(ch, "You have no idea how to enter such a protective stance.\r\n");
return;
}
/* 2. Optional: Add a failure chance if their practice is low */
if (rand_number(1, 101) > GET_SKILL(ch, SKILL_VANGUARD)) {
send_to_char(ch, "You try to enter a vanguard stance but lose your footing!\r\n");
WAIT_STATE(ch, PULSE_VIOLENCE);
return;
}
/* make sure class is a warrior */
if (!IS_WARRIOR(ch)) {
send_to_char(ch, "You don't have the training to lead a Vanguard.\r\n");
return;
}
if (AFF_FLAGGED(ch, AFF_VANGUARD)) {
REMOVE_BIT_AR(AFF_FLAGS(ch), AFF_VANGUARD);
/* Remove the AC bonus - assuming 20 points for tbaMUD/Circle scale */
GET_AC(ch) += 20;
send_to_char(ch, "You \tRdrop\tn your Vanguard stance.\r\n");
act("$n relaxes $s guard, looking less imposing.", FALSE, ch, 0, 0, TO_ROOM);
} else {
SET_BIT_AR(AFF_FLAGS(ch), AFF_VANGUARD);
/* Apply the AC bonus (Lower is better in traditional Circle/tba) */
GET_AC(ch) -= 20;
send_to_char(ch, "You enter the \tYVanguard\tn stance!\r\n");
act("$n plants $s feet, eyes fixed on every enemy movement.", FALSE, ch, 0, 0, TO_ROOM);
}
}
I think that will get most of it. There are some other files that need VANGUARD declared in like all the other skills, I just didn't include them here. I have refactored my damage() function as well. Here is the output from my imp looking at the fight with Haides and Tinkerfrost. Haides starts the fight as usual. Tinkerfrost cast meteor which is an AOE and she gets on the 2nd and 3rd mobs aggro table. Haides is not on theirs yet, but when he does "challenging shout" he gets on it and jumps ahead of Tinkerfrost.
My thought is to make the tank be an active player to some degree and not just have him sitting there spamming skills. He needs to watch to make sure someone else does not steal aggro off him, but yet not have him continuously having to spam taunt or challenging shout every other round too.
The fight log.
The meteor Tinkerfrost summoned upon Talon's head seems to have disintegrated. Oops!
A player will see the mob condition and who "that" mob is currently focused on. Not the greatest, but will give some idea at least for now. I'm sure there is a cleaner and more efficient way of implementing this. Let me know if I missed anything or you have more suggestions for me.