PDA

View Full Version : Adjusting Mob Damage in Real Time


trevius
09-01-2008, 06:33 AM
To make some more dynamic encounters, I thought it would be cool to have a way to adjust how hard a mob hits in real time. So, they could potentially get stronger (or weaker) as a fight progresses. I think the best way to do this would be via a quest command.

Maybe there is already a way to do this, but I can't seem to figure it out. If there isn't a way to do this, maybe someone could suggest a way to implement a new command code for quest objects. Basically, I would like to be able to add to or subtract X amount from every swing. I think the Damage quest object only does a 1 time set amount of damage, unless I don't fully understand how it works. But, something similar to that command that can adjust the set rate for every swing would be nice.

These are the related quest objects that I think could help for setting this:

GetMaxDamage(tlevel)
GetMaxDMG()
GetDamageAmount(tmob)
Damage(from, damage, spell_id, attack_skill, avoidable= true, buffslot= -1, iBuffTic= false)

And here is the source where I think the command would have to be added, but I am not completely sure yet:

attack.cpp

bool NPC::Attack(Mob* other, int Hand, bool bRiposte) // Kaiyodo - base function has changed prototype, need to update overloaded version
{
_ZP(NPC_Attack);
int damage = 0;

if (!other) {
SetTarget(NULL);
LogFile->write(EQEMuLog::Error, "A null Mob object was passed to NPC::Attack() for evaluation!");
return false;
}

if(DivineAura())
return(false);

if(!GetTarget())
SetTarget(other);

//Check that we can attack before we calc heading and face our target
if (!IsAttackAllowed(other)) {
if (this->GetOwnerID())
entity_list.MessageClose(this, 1, 200, 10, "%s says, 'That is not a legal target master.'", this->GetCleanName());
if(other)
RemoveFromHateList(other);
mlog(COMBAT__ATTACKS, "I am not allowed to attack %s", other->GetName());
return false;
}
float calcheading=CalculateHeadingToTarget(target->GetX(), target->GetY());
if((calcheading)!=GetHeading()){
SetHeading(calcheading);
FaceTarget(target, true);
}

if(!combat_event) {
mlog(COMBAT__HITS, "Triggering EVENT_COMBAT due to attack on %s", other->GetName());
parse->Event(EVENT_COMBAT, this->GetNPCTypeID(), "1", this, other);
combat_event = true;
}
combat_event_timer.Start(CombatEventTimer_expire);

SkillType skillinuse = HAND_TO_HAND;

//figure out what weapon they are using, if any
const Item_Struct* weapon = NULL;
if (Hand == 13 && equipment[7] > 0)
weapon = database.GetItem(equipment[7]);
else if (equipment[8])
weapon = database.GetItem(equipment[8]);

//We dont factor much from the weapon into the attack.
//Just the skill type so it doesn't look silly using punching animations and stuff while wielding weapons
if(weapon) {
mlog(COMBAT__ATTACKS, "Attacking with weapon: %s (%d) (too bad im not using it for much)", weapon->Name, weapon->ID);

if(Hand == 14 && weapon->ItemType == ItemTypeShield){
mlog(COMBAT__ATTACKS, "Attack with shield canceled.");
return false;
}

switch(weapon->ItemType){
case ItemType1HS:
skillinuse = _1H_SLASHING;
break;
case ItemType2HS:
skillinuse = _2H_SLASHING;
break;
case ItemTypePierce:
case ItemType2HPierce:
skillinuse = PIERCING;
break;
case ItemType1HB:
skillinuse = _1H_BLUNT;
break;
case ItemType2HB:
skillinuse = _2H_BLUNT;
break;
case ItemTypeBow:
skillinuse = ARCHERY;
break;
case ItemTypeThrowing:
case ItemTypeThrowingv2:
skillinuse = THROWING;
break;
default:
skillinuse = HAND_TO_HAND;
break;
}
}

int weapon_damage = GetWeaponDamage(other, weapon);

//do attack animation regardless of whether or not we can hit below
sint16 charges = 0;
ItemInst weapon_inst(weapon, charges);
AttackAnimation(skillinuse, Hand, &weapon_inst);

//basically "if not immune" then do the attack
if((weapon_damage) > 0) {

//ele and bane dmg too
//NPCs add this differently than PCs
//if NPCs can't inheriently hit the target we don't add bane/magic dmg which isn't exactly the same as PCs
int16 eleBane = 0;
if(weapon){
if(weapon->BaneDmgBody == other->GetBodyType()){
eleBane += weapon->BaneDmgAmt;
}

if(weapon->BaneDmgRace == other->GetRace()){
eleBane += weapon->BaneDmgRaceAmt;
}

if(weapon->ElemDmgAmt){
eleBane += (weapon->ElemDmgAmt * other->ResistSpell(weapon->ElemDmgType, 0, this) / 100);
}
}

if(!RuleB(NPC, UseItemBonusesForNonPets)){
if(!GetOwner()){
eleBane = 0;
}
}

int8 otherlevel = other->GetLevel();
int8 mylevel = this->GetLevel();

otherlevel = otherlevel ? otherlevel : 1;
mylevel = mylevel ? mylevel : 1;

//instead of calcing damage in floats lets just go straight to ints
if(RuleB(Combat, UseIntervalAC))
damage = (max_dmg+eleBane);
else
damage = MakeRandomInt((min_dmg+eleBane),(max_dmg+eleBane)) ;


//check if we're hitting above our max or below it.
if((min_dmg+eleBane) != 0 && damage < (min_dmg+eleBane)) {
mlog(COMBAT__DAMAGE, "Damage (%d) is below min (%d). Setting to min.", damage, (min_dmg+eleBane));
damage = (min_dmg+eleBane);
}
if((max_dmg+eleBane) != 0 && damage > (max_dmg+eleBane)) {
mlog(COMBAT__DAMAGE, "Damage (%d) is above max (%d). Setting to max.", damage, (max_dmg+eleBane));
damage = (max_dmg+eleBane);
}

sint32 hate = damage;
//THIS IS WHERE WE CHECK TO SEE IF WE HIT:
if(other->IsClient() && other->CastToClient()->IsSitting()) {
mlog(COMBAT__DAMAGE, "Client %s is sitting. Hitting for max damage (%d).", other->GetName(), (max_dmg+eleBane));
damage = (max_dmg+eleBane);

mlog(COMBAT__HITS, "Generating hate %d towards %s", hate, GetName());
// now add done damage to the hate list
other->AddToHateList(this, hate);
} else {
if(!other->CheckHitChance(this, skillinuse, Hand)) {
damage = 0; //miss
} else { //hit, check for damage avoidance
other->AvoidDamage(this, damage);
other->MeleeMitigation(this, damage, min_dmg+eleBane);
ApplyMeleeDamageBonus(skillinuse, damage);
TryCriticalHit(other, skillinuse, damage);

mlog(COMBAT__HITS, "Generating hate %d towards %s", hate, GetName());
// now add done damage to the hate list
if(damage != 0)
other->AddToHateList(this, hate);
else
other->AddToHateList(this, 0);
}
}

mlog(COMBAT__DAMAGE, "Final damage against %s: %d", other->GetName(), damage);

if(other->IsClient() && IsPet() && GetOwner()->IsClient()) {
//pets do half damage to clients in pvp
damage=damage/2;
}
}
else
damage = -5;

//cant riposte a riposte
if (bRiposte && damage == -3) {
mlog(COMBAT__DAMAGE, "Riposte of riposte canceled.");
return false;
}

if(GetHP() > 0 && other->GetHP() >= -11) {
other->Damage(this, damage, SPELL_UNKNOWN, skillinuse, false); // Not avoidable client already had thier chance to Avoid
}


//break invis when you attack
if(invisible) {
mlog(COMBAT__ATTACKS, "Removing invisibility due to melee attack.");
BuffFadeByEffect(SE_Invisibility);
BuffFadeByEffect(SE_Invisibility2);
invisible = false;
}
if(invisible_undead) {
mlog(COMBAT__ATTACKS, "Removing invisibility vs. undead due to melee attack.");
BuffFadeByEffect(SE_InvisVsUndead);
BuffFadeByEffect(SE_InvisVsUndead2);
invisible_undead = false;
}
if(invisible_animals){
mlog(COMBAT__ATTACKS, "Removing invisibility vs. animals due to melee attack.");
BuffFadeByEffect(SE_InvisVsAnimals);
invisible_animals = false;
}

if(hidden || improved_hidden)
{
EQApplicationPacket* outapp = new EQApplicationPacket(OP_SpawnAppearance, sizeof(SpawnAppearance_Struct));
SpawnAppearance_Struct* sa_out = (SpawnAppearance_Struct*)outapp->pBuffer;
sa_out->spawn_id = GetID();
sa_out->type = 0x03;
sa_out->parameter = 0;
entity_list.QueueClients(this, outapp, true);
safe_delete(outapp);
}


hidden = false;
improved_hidden = false;

//I doubt this works...
if (!target)
return true; //We killed them

// Kaiyodo - Check for proc on weapon based on DEX
if( !bRiposte && other->GetHP() > 0 ) {
TryWeaponProc(weapon, other); //no weapon
}

// now check ripostes
if (damage == -3) { // riposting
DoRiposte(other);
}

if (damage > 0)
return true;
else
return false;
}

void NPC::Damage(Mob* other, sint32 damage, int16 spell_id, SkillType attack_skill, bool avoidable, sint8 buffslot, bool iBuffTic) {
if(spell_id==0)
spell_id = SPELL_UNKNOWN;

//handle EVENT_ATTACK. Resets after we have not been attacked for 12 seconds
if(!attack_event) {
mlog(COMBAT__HITS, "Triggering EVENT_ATTACK due to attack by %s", other->GetName());
parse->Event(EVENT_ATTACK, this->GetNPCTypeID(), 0, this, other);
attack_event = true;
}
if(!combat_event) {
mlog(COMBAT__HITS, "Triggering EVENT_COMBAT due to attack by %s", other->GetName());
parse->Event(EVENT_COMBAT, this->GetNPCTypeID(), "1", this, other);
combat_event = true;
}
attacked_timer.Start(CombatEventTimer_expire - 1); //-1 to solidify an assumption in NPC::Process
combat_event_timer.Start(CombatEventTimer_expire);

if (!IsEngaged())
zone->AddAggroMob();

//do a majority of the work...
CommonDamage(other, damage, spell_id, attack_skill, avoidable, buffslot, iBuffTic);

if(damage > 0) {
//see if we are gunna start fleeing
if(!IsPet()) CheckFlee();
}
}

This is probably more for custom servers than anything, but I do think it would add more dynamics to some encounters. I am open to ideas if anyone has any :)

joligario
09-01-2008, 06:43 AM
Not necessarily just custom server. That is actually pretty relevant.

Example: Necromancer 1.5 Prequest. The Man-Eating Freshwater Shark is supposed to scale his hits as his health goes down. Starts off at 800s and hits 1200s near death.

I think that would be a good addition.

KLS
09-01-2008, 07:10 AM
I've actually wanted to implement a ModifyStat(const char *statname, int value) function for a while but just haven't gotten around to it.

janusd
09-02-2008, 07:36 AM
I'm afraid it's getting more and more necessary. Though we're not at GoD yet, that was the expansion when mobs started scaling in difficulty based on their HP. We COULD just depop and then repop the mob at certain HP% and then when the new one pops spawn it at a certain % all using PERL commands, but given that SPELLS wouldn't carry over, that could potentially and easily wipe a raid. Additionaly, if a script doesn't fire correctly, the original mob would remain which would mean an easier fight, and, depending on how we set up the loot tables, no loot once it goes doen.