Go Back   EQEmulator Home > EQEmulator Forums > Development > Development::Development

Development::Development Forum for development topics and for those interested in EQEMu development. (Not a support forum)

Reply
 
Thread Tools Display Modes
  #1  
Old 08-26-2014, 03:00 AM
Torven
Sarnak
 
Join Date: Aug 2014
Posts: 52
Default EverQuest Spell Resist Data Analysis

Greetings fellow EverQuest lovers. I am Torven of Al'Kabor/Project 1999 aka Torrid of Druzzil Ro/rerolled.org and this is my first post here. Since Al'Kabor's shutdown I have been motivated to assist in the preservation of my favorite game, and my contributions to the Al'Kabor Project have been primarily in the form of data collecting with a focus on combat mechanics accuracy and NPC statistics.

As Al'Kabor no longer exists, my collection efforts require me to obtain data from EQLive, (which is what I call the current modern incarnation of EverQuest to distinguish it from Al'Kabor or earlier eras) and since TAKP is merely an EQEmu fork with some classic reversions, most of my research also applies to EQEmulator's project. I concluded that this forum would probably be the best place to start posting my data, as I want all emus to benefit. I also want the information to be preserved and available on a long lived forum.

This post will be about spell resists. If this is well received I will author more posts on other game mechanics.

edit: to my great disappointment, img tags don't appear to be working. I will instead provide a link to each image where they are intended to be embedded. The entire album can be viewed here: http://imgur.com/a/Gtu2U

Abstract

This post is the result of four months of data collecting and analysis gathered from EQLive this year and some PvP data from Al'Kabor in the month before it was shut down. I have logged literally hundreds of hours of spell casts on dozens of NPCs and PCs of various levels. The goal was to collect enough data to determine a formula that would return a good estimate of an NPC's resist value from parsing logs of many spell casts on the NPC. It turned out that the prediction curve for spell resists is actually fairly simple for NPCs. (it's linear) The data I have collected will allow for an extremely accurate resist algorithm recreation.

This pastebin link: http://pastebin.com/CTFnUqcB is a text file with all of the processed results from my raw log files. The data consists of resist rates at incremental resist values of many NPCs.


Data Collection Methods and Tools

The reason my work was possible is because of two simple facts: Resist debuff values are known; and on targets equal in level to the caster, spells will always land if resist value == 0.

Knowing that, one can obtain the precise resist value of an NPC by incrementally reducing its resist value to the point of zero spell resists. Plotting a full curve of resist rates at many points of resist values then allows you to determine a NPC's resist value from one collection of spell casts, which is of course information of value for EQ database projects.

The procedure was simple: I leveled a shaman, a bard, and an enchanter to debuff mobs with. I equipped the bard with multiple percussion mod items and determined how much both Harmony of Sound and Occlusion of Sound lowered resists with each instrument. This allowed me to debuff mobs to a large number of resist values up to a maximum of -163 for non-magic resists. (my shaman is only level 90 and bard 80; level 100s could debuff further) In addition I had my original 15 year old 65 wizard copied over to the test server (where I collected my data) which allowed me to add the resist adjust of lure spells to the debuff value, allowing for a total debuff value of -463 for fire cold and magic. (tash would decrease that further for MR)

The next step was to cast thousands of spells on a NPC at incrementally reduced debuff values, to determine the mob's absolute resist value. Due to the nature of the RNG, you really need something approaching like 10k casts to reduce the margin of error to 1%. It quickly became apparent that 30 minute parses would not suffice. Most plots you see will have been from 90 minute or longer parses. (some as long as 4 hours) Since spells took about 5 seconds each, a 1% margin of error parse would take half a day, so the lines won't be perfectly straight or gracefully curved. (particularly for the lure parses, which required me to lose aggro and allow the mob to heal several times)

My first tool I created was an autoit script I wrote to automate spell casting. I wrote it with multiple casting modes to include also possibly slowing the NPC and casting an aggro spell (disempower generally) to allow my other spell casters to not overaggro my level 90 shaman. It has an adjustable casting delay.

My second tool was a parser in the form of a lua script to read the logs and spit out the relevant numbers. I wrote three versions: one to parse spells done to the player, one to parse all-or-nothing spells done to a target, and one to parse direct damage spells done to a target.

My third tool was the in-game item Staff of Temperate Flux. This item is an instant cast clickable that places a small debuff on the target (-6 CR and FR), usable only by wizards. This item allows for a near order of magnitude more spell casts over time compared to any cast spell, allowing for much more precise resist rate data. The spell it casts is called 'Lower Element I'.

To obtain NPC vs PC data, I created a magician to summon Fireblades, which is a level 66 summoned weapon that procs a fire DD at twice the normal rate. I handed two of those to an NPC and let it beat on me for four hours (or more) per log at specific fire resist values (usually 100).

The raw data (the log files) and the scripts I wrote can be obtained from my google drive here: https://drive.google.com/folderview?...p=sharing#grid


Analysis - PC vs NPC

As you may have seen already, this post will contain many line graphs. This is easily visualized data, and graphs make certain conclusions obvious.

The first graph I will present is one of a level 65 EarthB zone NPC: A Bloodthirsty Moss Warlord. I selected this NPC because it has a relatively high magic resist for a readily available static level NPC. (it's not easy finding mobs with a MR high enough to resist many casts undebuffed, but low enough that obtaining the precise resist value is doable) I favor MR so the temperate flux staff can be used; also many classes have an all-or-nothing MR spell, allowing my enchanter, wizard, and shaman to combine data on the same targets.

http://i.imgur.com/Rjme44p.png

The first thing you will notice is that the resist function on NPCs is linear, even at different levels from the target. This is significant because it means the 'curve' is as simple as it gets, making estimating the resist value of NPCs from the resist rate a very simple calculation.

Secondly you will notice that every two resist points translates to 1 percent of resist. This means that NPCs (without debuffs on) with over 200 resist become immune to all-or-nothing spells, assuming said spells had no 'lure' adjustments.

Third, as expected, resist rates change when the caster is not the same level as the NPC-- at least on level 65 NPCs.

Fourth, the resist chance hits zero at -71 for an equal level caster. 71 is an odd number, but I got resists at 70, so I'll just have to accept that 71 may be the value even if most NPCs are usually a multiple of 5.

There is a problem with using a Temperate Flux staff however; the staff's debuff does not stack with bard debuffs. To get parses at debuff levels that a shaman alone could not reduce to, I had to stack a bard debuff with the shaman debuff, which made the staff unusable. In that case I used a low level magic DD instead. Full hits on direct damage spells have the same resist rate as an all-or-nothing spell hit.

Evidence to back that claim can be found in the pastebin link above, but here are a couple of examples from this particular NPC:

[ 0]Lower Element I lvl65 - Hits: 10216 (64.641%) Resists: 5588 (35.358%)
[ 0]*Shock of Lightning lvl65 - Full hits: 774 (63.494%) Partials: 299 (24.528%) Full Resists: 146 (11.977%) Actual vs. Potential Dmg: 76.2%

[-70]Lower Element I lvl65 Malosinia(2) - Hits: 7259 (98.963%) Resists: 76 (1.036%)
[-70]*Shock of Lightning lvl65 Malosinia - Full hits: 833 (99.522%) Partials: 4 (0.477%) Full Resists: 0 (0%) Actual vs. Potential Dmg: 99.6%

Note that the DD parses of course have a larger margin of error, having much fewer casts.


Here's a graph of another NPC showing similar results:

http://i.imgur.com/znuamSO.png

Data for NPCs not included in this post can be found in the pastebin link.

Next, a graph showing resist rates of casters casting on equal level NPCs at various levels:

http://i.imgur.com/g1yxqUQ.png

This suggests that the per point effectiveness of resist does not change with level.

My next graph will be one of direct damage spells:

http://i.imgur.com/S9g62Cj.png

This NPC has very high fire resist rate, but not so high as to make it immune, which makes it useful to parse large debuff values on.

The 'Realized Dmg%' line is how much damage was done over how much potential damage could have been done had every spell landed for full damage. For example, if a spell does 250 damage, and I cast two spells that hit for 150 and 0 (full resist) then the realized damage would be 150/500 or 30%.

Lure spells were cast on this NPC which dramatically reduced the number of spells cast at the more extreme resist debuff values, resulting in an increased margin of error, but the linear nature is still evident. From this we can see that the ratio of full resist percent / resist value is 1/6, meaning that for DD spells, every 6 resist points increases the chance to full resist by 1%. The inverse being that every 6 debuff value increases the chance to hit (full or partial) by 1%. This means that NPCs become immune to non-lure DD spells above 600 resist.

http://i.imgur.com/aiyY9Tv.png

Here's a NPC with ~200 cold resist, which I selected because it allowed me to debuff the entire full hit range. The data has too much error to know if the actual resist value is 210 or 215 or somewhere in between, but we can be reasonably certain it's around there. Note that realized damage becomes much more linear below 200 resist value.

Now data of a level 68 NPC:

http://i.imgur.com/mwHC4CW.png

Notice that the level 65 and level 90 characters have a nearly identical resist rate. (within the margin of error) This is because the rules change at level 67. NPCs level 67 or above stop being more or less susceptible to PCs level 65 or above; meaning that starting at level 65, everybody's resists end up being the same on NPCs level 67+. Lower levels also have some special considerations according to the developer posted pseudocode (see below), but I did not parse low level NPCs.

Raid bosses follow the same rules.

http://i.imgur.com/v9KcZrg.png


How Level Affects Resist Rate

Most of the above graphs were on level 65 mobs; that was to eliminate level difference as a factor using my 65 wizard. Now I'll show you how level difference affects the rate spells are resisted. These two graphs are the result of many logs.

http://i.imgur.com/aLPicCS.png

Here we see resist rates on various NPCs by PCs level 50 to 70. Some of those plots were obtained from an enchanter so they will not be as precise as the wizard parses.

What we can determine from this is that there is a limit as to how much advantage a player gets from being a higher level than the NPC-- that limit is 20% resist rate, or 40 resist value. Resist rate flattens starting at +9 levels above the NPC-- the level advantage players get over NPCs is capped at 9 levels, and 9 levels corresponds with 20% resist rate. The second item of note is that the rate of change is not linear, and gets stronger the farther away from the NPC's level one is. The warlord and soldier resist rates hit 100% at their two points due to the maximum allowable level hit range.

The sentry and soldier lines flatten at 65 and stay flat; my level 90 had the same hit rate on those NPCs. The warlord however, as it is below level 67, still becomes more susceptible to spells beyond level 65 up to a cap of 20% as expected. (my level 90 shaman parsed a hit rate of at 83.9% on a warlord)

Here is the same data graphed another way:

http://i.imgur.com/FgQnaiB.png

At first glance this looks like a cubic function. I ran it though a curve fitting program and came up with a cube function that matched it pretty closely. (Y = -X + -0.014 * X^3) However it's actually a square function with the sign reversed for positive X: Y = X^2 / 4.

67+ mobs seem to follow a slightly different curve however. Note that higher level NPCs will not have a curve at all, as the highest level NPC a level 65 can hit is 86.


Resist Level Caps

It is commonly known that NPCs far above the player in level will resist 100% of spells. I took the liberty to figure out what the cap is at certain levels.

Level 55 may hit up to 68 (+13)
Level 60 may hit up to 70 (+10)
Level 61-63 may hit up to 75 (+14 to +12)
Level 64 may hit up to 80 (+16)
Level 65 may hit up to 86 (+21)

This information is very time-consuming to gather, so I didn't bother getting more than this. It's worth noting that the highest raid boss level in PoP is level 80, many other PoP raid bosses are level 75, and tier 1 PoP raid bosses are level 70, suggesting that this hasn't been altered since then. Also, the top raid bosses in Velious are level 70.


Maximum Resist?

It seems SOE liked to use 1000 resist value to make mobs 'immune'. Several spells have a -1000 resist adjust modifier (such as Dictate or Seru's AoE), and Lure of Flame began to (barely) hit a Sentry of Ro in the Tower of Solusek Ro after debuffing its fire resist -163 points. Assuming it has 1000 fire resist, that combined resist adjust would bring it to 537 which is below the immunity threshold for direct damage spells (600).

[-463]Lure of Flame lvl65 MaloseneRk2+-58HoS - Full Hits: 0 (0%) Partials: 13 (6.074%) Full Resists: 201 (93.925%) Actual vs. Potential Dmg: 0.1%


Analysis - NPC vs PC

So, we now have a good idea of what PC vs. NPC resists look like, but what about NPC vs. PC? Is it the same function/curve?

http://i.imgur.com/52E5D93.png

It appears so.

I obtained this data by handing NPCs procing weapons, and tanked the NPCs for hours. These parses were more difficult to get so I didn't feel the need to do more than this. There is one observable difference however: unlike PCs, NPCs do not have a level limit that caps the advantage or disadvantage they get from level difference. (or if there is a cap, it's significantly larger)


Analysis - PvP

Spells cast in PvP have extra constraints and different calculations done which result in a different curve than spells cast on NPCs. Allakhazam's Lucy spell database lists three pvp fields: 'pvpresistbase', 'pvpresistcalc', and 'pvpresistcap'. Lucy added these fields on May 12, 2004, but the PvP curve was different than the PvE curve even before then.

In the month before Al'Kabor shut down, I gathered as much data as I could in an attempt to preserve the classic game for emulation. One of the things I did was parse spell resists in the arena. I parsed Ensnare and Ignite (a druid DD) on an equal level player for 1 hour at various resist levels. Later I parsed Ensnare on EQLive.

http://i.imgur.com/dbF05ux.png

As you can see, PvP resists follow a curve instead of a line, and the curves from both servers match to a large degree, but only up to about 200 resist. The first unexpected result is that Ensnare actually resisted LESS at very high resist levels on Al'Kabor; it appears that PvP resists were bugged there-- or poorly implemented at least.

The EQLive ensnare curve has two differences: the rate of resist is capped at 95%, and resists don't begin until 10 MR. Ensnare has a -10 'pvpresistcap' value in the Lucy database. 'pvpresistcap' is not actually a cap at all-- it's a resist adjust similar to lure spells. One graph will not satisfy as definitive proof of that claim, so here's a graph of Disempower from EQLive:

http://i.imgur.com/tvCfdgF.png

Note that the resist rate at 0 resist value is about 13% (parsed at 12.879%). Disempower has a pvpresistcap of 17.

Here is a graph of disempower adjusted (moved forward) by 17 resist, along with Al'Kabor's Ensnare, and a parse done on a NPC by a caster 13 levels below the NPC's level:

http://i.imgur.com/MUtrNGo.png

Here is a PvP graph of Lower Element I:

http://i.imgur.com/sTGoHjV.png

It's nice and smooth since the spell is instant cast, allowing for large data sets to be collected in a short period of time. Since Lower Element I has a pvpresistbase of 0, a pvpresistcalc of 100, and a pvpresistcap of 0, you can see that it begins at 0 and the curve matches the Al'Kabor curve up to around 190 resist, where it still caps at 95% resist rate.

Here is a graph showing the change in resist rate as a function of level difference from the target:

http://i.imgur.com/C0KRt7Q.png

Again note that the resist rate caps out at 95%, so the last four plots actually wouldn't be flat if the resist value I had chosen were lower. (perhaps I should have left them out)

Now for some direct damage graphs.

http://i.imgur.com/EkcpYIf.pnghttp:/...om/cs8ueSp.png

The Frost Rift parses are 4 hours long and consist of 3,500+ casts each. The Ignite parses were about 1 hour and 700 casts each, which is why those lines are so crooked. (the server was shutting down in days and I didn't have the time) The margin of error at 700 is something like 4%, but I think the algorithm the server used was also pretty wonky.

Frost Rift has a significant resist adjust (pvpresistcap) of 37, and caps out around 93% resist rate. It has a 'pvpresistbase' of 50 but I couldn't figure out how that number applied to the results I was seeing.

Al'Kabor's bugged resist curve is also evident in the Ignite results.

http://i.imgur.com/O9KzFq0.png

Here are full hit plots of Frost Rift shifted over to the right by 37 resist, along with Ignite and the great white shark plot shifted left by 15 for comparison.


EQLive2014 vs. Old EQ

Since my task is to assist in the recreation of an earlier era EQ (PoP specifically), I needed the answer to one question: are the resists in today's EQ the same as they were in PoP era EQ? Unfortunately it did not occur to me that the PvE and PvP resist curve might be different when I was hurrying to collect as much data as I could before the Al'Kabor shutdown, so I only did PvP parses. I do however have many raid logs from my time playing EQ in 2003.

I have almost every time raid that my guild did logged, which spans 9 months or so. Since I was also either the MT or 2nd in line, I almost always (if not always) had a bard playing Psalm of Veeshan, and my gear was rather good, so my resists were almost always capped. This means I have a significant sample size of raid boss AoEs on a (mostly) known resist value character to search from. A simple grep tool can easily reveal and count the desired text. Here's the tally:

Level 71 NPCs
-------------
Time Terris AoE Phantasmal Torment resist adjust: -300 (magic); resists: 115 (85.2%); hits: 20 (14.8%)
Time Saryrn AoE Horrifying Affliction resist adjust: -300 (disease); resists: 232 (82.3%); hits: 50 (17.7%)
Time Saryrn AoE Torrent of Agony resist adjust: -300 (poison); full resists: 66 (33.2%); partials: 121 (60.8%); full hits: 12 (6%)
Time Terris AoE Quivering Nightmares resist adjust: -400 (magic); resists: 119 (36.6%); hits: 206 (63.4%)
Time Saryrn proc Torrential Torment resist adjust: -450 (poison); resists: 67 (19%); hits: 286 (81%)

Level 75 NPCs
-------------
Black Plague: Time Bertoxxulous AoE; 60s recast; -300 disease resist adjust; resists: 73 (70.9%); hits: 30 (29.1%)
Rain of Bile: Time Bertoxxulous) AoE (rain); 60s recast; -300 poison resist adjust; resists: 209 (66.8%); hits: 104 (33.2%)
Rage of Zek: Time Rallos Zek and Drunder Rallos Zek the Warlord (both lvl75) AoE; 35-48s recast; -100 fire resist adjust; resists: 216 (97.3%); hits: 6 (2.7%)

Now you know why I selected level 71 and 75 NPCs to parse NPC vs PC resists. It's a simple matter to plot these on a EQ2014 graph:

http://i.imgur.com/V5E9GmT.png

The evidence is convincing that the resist function has changed little in all this time. But wait, the Rallos AoE should be 0%, you say. Well that bothered me too, so I checked for instances of bard song dropping before the AoEs landed, and sure enough I found them:

time again.txt 152 [Tue Jan 13 19:04:31 2004] The crystalline scales fall away.
time again.txt 167 [Tue Jan 13 19:04:37 2004] The chaos of war consumes your soul.

time other's hits.txt 47232 [Sat Jan 24 20:27:56 2004] The crystalline scales fall away.
time other's hits.txt 47314 [Sat Jan 24 20:28:02 2004] The chaos of war consumes your soul.

time other's hits.txt 52463 [Sat Jan 24 20:30:52 2004] The crystalline scales fall away.
time other's hits.txt 52484 [Sat Jan 24 20:30:53 2004] The chaos of war consumes your soul.

time yet again.txt 50689 [Fri Jan 30 21:26:33 2004] The crystalline scales fall away.
time yet again.txt 50909 [Fri Jan 30 21:26:37 2004] The chaos of war consumes your soul.

The other two hits I took were after I was newly resurrected before Rallos was killed. All 6 hits were explainable.

Further Evidence: Rydda`Dar

Rydda`Dar is the dragon in Halls of Honor. It's slowable, but highly resistant; Rydda is notoriously hard to land slow on, and this claim is easily verified by googling. This makes it a good candidate to test the hypothesis that the current resist curve is the same as the PoP era, because knowing it was hard to slow gives us an idea as to its resist value back then.

Resist debuffs have remained unchanged, so if Rydda still remains hard to slow with PoP era debuffs on in 2014, then that is evidence that the curve was unaltered. Is that the case? I parsed it to find out.

[-153]Lower Element I lvl65 -48Tash+MaloseneRk2 - Hits: 1005 (8.636%) Resists: 10632 (91.363%)
[-172]Disempower lvl90 -48Tash+Malis+-58HoS - Hits: 266 (17.627%) Resists: 1243 (82.372%)

These parses indicate a Magic Resist value of 335 for Rydda`Dar. (91.363 * 2 + 153 = 335.726)

A PoP era debuff level would be -50 from Howl of Tashan (my enchanter is only level 61), -55 from Malos, and ~-50 from Harmony of Sound. I don't have a 16 focus drum to get the exact value, but 50 should be very close. As far as I know, 16 was the best Luclin era drum. (drums of the beast) Malos and Harmony of Sound are both rune turn-in (level 65) spells. The EQ2014 curve would put Rydda at a 10% chance for slow to land with -155 MR debuffs.

It is important to highlight that making an NPC very difficult (and still possble) to slow requires giving it an MR value in a narrow range; since 2 resist value == 1 resist rate %, you have to know the maximum possible debuff value combined, add it to 200, then subtract 2 for every 1% chance you want to allow for the possibility of slow landing. The difference between unslowable and 10% chance for slow to land is a mere 20 resist value.


Earlier Eras

But what about eras earlier than PoP? This is easy to answer: the above does not apply to older eras. Shortly before PoP launched, Sony revamped the resist system. Here are some relevant patch notes regarding changes to the resist system:

Quote:
- The level-based spell resistance bonus inherent in super-high level NPCs has been reduced significantly. (October 8, 2001)
Quote:
- Higher level PCs will be more resistant to lower level NPCs' spells. (September 4, 2002)
Quote:
September 4, 2002
** Resistance Changes **

We've made some fairly drastic changes to the way the spell resistance
system works. Previously, there was only the smallest benefit to having
resists over a certain value. We've reworked resistance in its
entirety, completely replacing the old system with one that is more
logical.

The idea behind the changes is pretty simple: Resists should matter in
a way that makes sense.

Important things to note about the new resistance system:

- Resists matter more for PCs. There are now tangible differences
between having 50, 150, and 250 in a given resistance, for example.
Resistance buffs, bard songs, and resist gear have actual value, all
the way up the line.

- Conversely, resistances also matter more for NPCs. Some NPCs became
more vulnerable to things they have always been vulnerable to, other
NPCs became more resistant to things that they were inclined to be
somewhat resistant to.

- Resistance debuffs should also have more value, all the way up the
line. For the first time, resistance debuffs now have the ability to
bring NPCs that were lure-style only down into the range of being hit
by normal spells.

- The hard level limit involving players casting on NPCs has been
removed. This used to be referred to in EQ folklore as the "Six Level
Limit" (It was actually 1.25 times the caster's level, but more people
likely thought about it the other way.) This means that in the vast
majority of cases, there is at least a small chance that a person will
be able to connect a spell with an NPC, even if they are out of that
NPC's traditional level range.

- Overall, against NPCs that have medium-to-high resistances of a given
type, expect to see more full hits, fewer partials, but more full
resists in the new system. Taken over time, the damage done by PC
casters to semi-high resistance NPCs should be approximately the same,
but will definitely improve when the proper debuffs are applied (we
wanted to make sure that this did not turn into a universal nerf of
casters).

September 26, 2002
- The recent resist changes have been adjusted for PvP. They will not
be exactly as they were prior to the resist change, but they should be
reasonable now. Please let us know if you feel they need further
adjustment.
Speculation: could the PvP curve be the pre-revamp PvE curve? (were they once the same?) I don't think this is possible to prove without the old source, sadly.


Conclusions
  • Spells landing on players from players have a different resist curve than spells landing on or cast from NPCs.
  • The PvP curve is slightly convex or concave (depending on how you graph it) while the PvE curve/function is linear.
  • Al'Kabor (EQ circa early PoP) and modern EQ have the same PvP resist curve up to 200 resist. Beyond that is dissimilar due to Al'Kabor's seemingly bugged upper end curve.
  • Allakhazam's 'pvpresistcap' field in its Lucy spells database appears to not be a cap at all, but rather a resist modifier added to spells when cast on players, similar to 'resist adjust' for lure-type spells.
  • NPCs casting on PCs follow the same resist curve/line as PCs on NPCs, but NPCs are not level advantage capped at 9 as PCs are.
  • NPCs gain the resists from the items in their inventories, including armor, but may be limited to one per equipment slot. (unconfirmed but likely)
  • There is a hard level limit above the player after which mobs become completely immune to any and all spells; this limit varies with level.
  • The hit chance of an all-or-nothing spell is the same chance a direct damage spell will full hit. Direct damage spells will never full hit beyond 200 resist.
  • On NPCs of equal level to the caster, the chance to fully resist is mob resist value / 2.
  • This means that mobs of equal level having a resist value over 200 will always resist an all-or-nothing spell, and any DD spell that hits after 200 resist will always be a parial, non-full hit.
  • On NPCs of equal level to the caster, the chance to fully resist direct damage spells is mob resist value / 6.
  • This means that mobs of equal level with greater than 600 resist value will always fully resist a direct damage spell.
  • The advantage or disadvantage from level difference is: resist adjust = level difference^2 / 2, with the sign flipped if the caster is higher level.
  • Level advantage is capped at 9 levels for PCs casting on NPCs, which translates to 20% resist chance or 40 resist value.
  • On NPCs at or above level 67, if the caster is level 66 or higher, then the level difference between the two is ignored. i.e. it calculates as a white con.
  • EQ's resist algorithm has not changed much since the resist system revamp in late Luclin.

Resist Algorithm Pseudocode And EQEmu's Implementation

EQ's (now) Lead Designer, Prathun, posted some pseudocode (or something close to pseudocode) of the resist function on the EQ boards a few years ago. The post is gone now (or moved somewhere I don't know where) but Allakhazam archived it here:

http://everquest.allakhazam.com/foru...01109310546959

It's a bit long but I'll quote it here for preservation and discussion purposes.

Code:
Pull the spell's resist modifier from the spell (or the spell list override, if one exists). 
Adjust the resist modifier for applicable focus effects. 
Check for fear immunity.  If roll is made, resist spell. 
Check for resistance to the spell effect.  If roll is made, resist spell. 
Check for Sanctification.  If roll is made and spell is not no_resist, resist spell. 
Calculate target's resistance chance applicable to this spell.   
     If spell's resist type is no_save, spell lands.   
     Otherwise, magic checks against magic, fire against fire, chromatic checks lowest, prismatic checks average, etc.  The capped resistance score is used. 
Set resist chance to 15 if the spell effect is a lull. 
Adjust resist chance for level difference between caster and target. 
     Set temp level difference to (target level - caster level). 
     If target is at least level 67 and target is an NPC, temp level difference is set to (66 - caster level) or 0, whichever is greater. 
     If target is a PC, and caster level is at least 21, and temp level difference is greater than 15, set temp level difference to 15. 
     If target is an NPC, and temp level difference is less than -9, set temp level difference to -9. 
     Set level modifier to (temp level difference * temp level difference / 2) 
     If temp level difference is negative, make level modifier negative. 
     If target is an NPC and caster is far below target's level, set level modifier to 1000. 
     Add level modifier to resist chance. 
Adjust resist chance for spell's resist modifier. 
If effect is damage and target is a non-mercenary NPC... 
     If target is at least level 67, level difference is set to (66 - caster level) or 0, whichever is greater. 
     If target is at least level 17 and level difference is greater than 0, add (2 * level difference) to resist chance. 
If resist chance is greater than spell's max resist and the max resist is not 0, set the resist chance to max resist. 
If resist chance is less than spell's min resist and the min resist is not 0, set the resist chance to min resist. 
Roll a random number between 0 and 200. 
If the roll is greater than the resist chance, spell lands. 
If the roll is not greater than the resist chance and the spell does not allow partial resists, resist spell. 
If spell effect does not apply damage, spell lands. 
Otherwise, spell effect applies damage.  Calculate partial resist. 
     If the resist chance is less than 1, set the resist chance to 1. 
     Partial resist modifier is set to ((150 * (resist chance - roll)) / resist chance). 
     If target is a non-mercenary NPC... 
          If target is higher level than caster, and target is at least level 17, and caster is level 50 or below, add 5 to partial resist modifier. 
          If target is at least level 30 and caster is level 50 or below, add (casterlevel - 25) to partial resist modifier. 
          If target's level is less than 15, subtract 5 from partial resist modifier. 
     If caster is an NPC... 
          If target is at least 20 levels higher than caster, add (level difference * 1.5) to partial resist modifier. 
     If partial resist modifier is less than 0, set partial resist modifier to 0. 
     If partial resist modifier is greater than 100, set partial resist modifier to 100. 
     Spell lands.  Partial resist modifier is used to calculate resulting damage.
In the above pseudcode, 'resist chance' is actually the resist value. Since SOE uses a simple linear function, the two are closely related but the distinction is important.

I am aware that efforts were made to change EQEmu's algorithm to match this, and I must admit that I thought EQEmu's implementation wasn't as accurate as it is before I had spent all this time. However I did some cursory parses on my local server, and while the all-or-nothing resist rates seem to be correct, partial resists are way off. Spells are landing for full even after 200 resist, realized damage is off, partials always seem to do near full damage, and mobs seem to become immune to direct damage spells somewhere around 400 resist.

Using my research and examining this pseudocode allowed me to construct a basic but accurate resist simulator. Here is the core function in lua:

Code:
function resist(resistValue, maxSpellDmg)
	local roll = math.random(0, 200);
	
	if ( roll >= resistValue ) then			-- needs to be >= so 0 rolls do full damage at 0 resist
		return maxSpellDmg;
	else
		partialResistMod = (150 * (resistValue - roll)) / resistValue;
		
		if ( partialResistMod <= 0 ) then
			return maxSpellDmg;
		elseif ( partialResistMod >= 100 ) then
			return 0;
		end
		
		return maxSpellDmg - (maxSpellDmg * (partialResistMod / 100));
	end
end
Obviously I left out a whole bunch of stuff; this is just to generate the curves for direct damage spells.

Here is the output of that function, with 100,000 casts at every 10 resist values:

http://i.imgur.com/fWQj9XQ.png

Fixing EQEmu's spells.cpp for DD partials on NPCs should be straightforward at this point, but I've not used github before so I'll leave it to more capable hands. I've not tested PvP resists on EQEmu however, so I have no idea what state that is in.


How to Determine an NPC's Resist Value

Here is how you determine an NPC's resist value for Project EQ, TAKP, or other databases:

Log hundreds if not thousands of spell casts on the NPC with zero debuffs on it. The more casts, the better. I'm not a statistician, but if I'm reading wikipedia right, getting a margin of error within 1% at a 95% confidence requires nearly 10 thousand casts, 2% requires 2,401 casts, 3% requires 1067 casts, 4% 600 casts, and 5% 384 casts. Low damage direct damage spells are preferable as they can indicate a resist value up to 600 instead of 200.

The following is true if the NPC is a white con, or the NPC is level 67 or higher and the caster is level 65 or higher:

If the spell is an all-or-nothing spell
If resist rate < 100%
resist value = resist rate * 2.
Else spell was 100% resisted
resist value is > 200

Else the spell is a direct damage spell
If full resist rate < 100%
resist value = full resist rate * 6.
Else spell was 100% fully resisted
resist value is > 600

To get resist values > 200 or 600, debuff the NPC as much as possible while logging casts on it. (be very careful to not let any debuffs drop) Then calculate the resist rate. You simply add the absolute value of the resist debuffs to the resist value you get. I.e. if the debuff does -70 resist, add 70. If spells still 100% resist, then you can be sure the resist value is either 200+abs(debuff value) or 600+abs(debuff value). Be sure to factor in resist adjusts of the spell cast (i.e. -300 for wizard lure spells).

If the NPC is not equal level to the caster, and the NPC is below level 67 or the caster is below level 65 you need to factor in the level difference. The easiest way to do this is use a caster 9 or more levels higher than the NPC, and add 40 resist to the resist value. The level advantage caps at +9, and 9 levels translates to 40 resist. If your caster is 1-8 levels higher, add the result of this: level difference * level difference / 2.

Here's an example:

The Statue of Rallos Zek (level 59) has a magic resist of about 260. Here's how I got that number.

I parsed it with a -105 debuff on, as it was resisting 100% without debuffs:

Statue of Rallos Zek Lower Element I lvl65 MaloseneRk2 - Hits: 4834 (30.664%) Resists: 10930 (69.335%)

69.335 * 2 + 105 = 243.67

The caster I used is 6 levels above the NPC, so I calculate the resist advatage from level: 6 * 6 / 2. 6 levels translates to 18 resist value, or 9% resist chance. So I add 9 to the resist rate, which tells me how it would look if I were the same level as the NPC:

78.335 * 2 + 105 = 261.67

or you can just add 18 resist value:

69.335 * 2 + 123 = 261.67

Lastly I round to the nearest multiple of 5, since these numbers tend to be round.
Reply With Quote
  #2  
Old 08-26-2014, 06:50 AM
KLS
Administrator
 
Join Date: Sep 2006
Posts: 1,348
Default

This is a lot of info, but it seems like you've really put work into it so tomorrow I'll look at see what's up with the DD partials. People have always said they thought they were a bit off but could never prove where/why.
Reply With Quote
  #3  
Old 08-26-2014, 09:52 AM
haynar
Developer
 
Join Date: Jul 2009
Location: In a state of bliss
Posts: 31
Default

Very nice. Good job.

Haynar
Reply With Quote
  #4  
Old 08-26-2014, 12:01 PM
demonstar55
Demi-God
 
Join Date: Apr 2008
Location: MA
Posts: 1,165
Default

Our current code is based off Dev quotes, and I know everytime kayen went and parsed stuff related to resists, his predictions using our code came out correct. Now current vs old, that can still be off, and maybe partials, I'd have to look over the code again, and finish reading this post :P

I think the biggest issue is actually mob resist data
Reply With Quote
  #5  
Old 08-26-2014, 07:59 PM
KLS
Administrator
 
Join Date: Sep 2006
Posts: 1,348
Default

I've heard from enough sources that partials seem "wrong" that I think it's worth looking into is all =p
Reply With Quote
  #6  
Old 08-26-2014, 08:36 PM
demonstar55
Demi-God
 
Join Date: Apr 2008
Location: MA
Posts: 1,165
Default

Yeah, the code also says its mostly right but more often leads to full, so ... That sounds like partials are intentionally wrong :P
Reply With Quote
  #7  
Old 08-27-2014, 03:58 AM
vsab's Avatar
vsab
Discordant
 
Join Date: Apr 2014
Location: United Kingdom
Posts: 276
Default

I love this kind of info so keep it coming
Reply With Quote
  #8  
Old 08-27-2014, 05:54 PM
Torven
Sarnak
 
Join Date: Aug 2014
Posts: 52
Default

I have a proposed fix. I suppose I should have done this before posting, but I'm trying to keep my tasks narrowed to data collection.

Change the end of Mob::ResistSpell() to this:

Code:
	//Finally our roll
	int roll = MakeRandomInt(0, 200);
	if(roll >= resist_chance)
	{
		return 100;
	}
	else
	{
		if(!IsPartialCapableSpell(spell_id))
		{
			return 0;
		}
		else
		{
			if (resist_chance < 1)
			{
				resist_chance = 1;
			}

			int partial_modifier = ((150 * (resist_chance - roll)) / resist_chance);

			if(IsNPC())
			{
				if(GetLevel() > caster->GetLevel() && GetLevel() >= 17 && caster->GetLevel() <= 50)
				{
					partial_modifier += 5;
				}

				if(GetLevel() >= 30 && caster->GetLevel() < 50)
				{
					partial_modifier += (caster->GetLevel() - 25);
				}

				if(GetLevel() < 15)
				{
					partial_modifier -= 5;
				}
			}

			if(caster->IsNPC())
			{
				if((GetLevel() - caster->GetLevel()) >= 20)
				{
					partial_modifier += (GetLevel() - caster->GetLevel()) * 1.5;
				}
			}
			
			if(partial_modifier < 0)
			{
				return 100;
			}

			if(partial_modifier > 100)
			{
				return 0;
			}

			return 100 - partial_modifier;
		}
	}
and edit Mob::SpellEffect() in spell_effects.cpp to prevent 0 damage spells, like so:

Code:
				// take partial damage into account
					dmg = (int32) (dmg * partial / 100);

					if (dmg == 0)
						dmg = -1;

					//handles AAs and what not...
					if(caster)
						dmg = caster->GetActSpellDamage(spell_id, dmg, this);
					
					dmg = -dmg;
					
					Damage(caster, dmg, spell_id, spell.skill, false, buffslot, false);
This is actually closer to Prathum's pseudocode. Whoever wrote EQEmu's version did a couple of strange things, which are substracting the roll from resist value before calculating the partial damage percent and switching around a couple of variables in the partial_modifer equation.

EQ Emu's curve currently looks like this: http://i.imgur.com/h9Xdixr.png

In Prathun's psuedocode, partial_modifer is actually the percent of RESISTED damage, but ResistSpell() returns a value that is the percent of the NON-RESISTED damage, which is why partials on EQ Emu tend to always be nearly full damage at high resist values. At high resist values, spells should actually never do near full damage.

Lastly I changed roll > resist_chance to roll >= resist_chance because a NPC with 0 resist value should never resist, and I have a log at 200 resist value that included full damage hits.

I tested and parsed this code on my local server and it seems more or less identical to live.
Reply With Quote
  #9  
Old 09-08-2014, 06:05 AM
KLS
Administrator
 
Join Date: Sep 2006
Posts: 1,348
Default

I didn't even see this last post but yeah I went through tonight and double checked things and basically came out with that code (doh duplication of effort).

I've tested it and it seems to work right which is encouraging. It's on master now.
Reply With Quote
  #10  
Old 09-08-2014, 06:32 PM
Torven
Sarnak
 
Join Date: Aug 2014
Posts: 52
Default

I noticed you didn't update spell_effects.cpp to prevent 0 damage spells. Did using a float literal when subtracting partial_modifier prevent those? I didn't think to do that because C++ isn't my preferred language.

Before I changed spell_effects.cpp, I was getting spell hits that would display the emote text but not display any non-melee damage text with it. (because it was 0 damage)
Reply With Quote
  #11  
Old 09-08-2014, 06:45 PM
KLS
Administrator
 
Join Date: Sep 2006
Posts: 1,348
Default

A 0 spell effective spell even if it's a partial capable should just react as a full resist.
Reply With Quote
  #12  
Old 09-08-2014, 10:18 PM
Torven
Sarnak
 
Join Date: Aug 2014
Posts: 52
Default

I just ran your code and it has the same problem mine had before I changed spell_effects.cpp.

If you cast a low level DD spell on a mob with near-immunity, it will do zero damage, show no non-melee text, and instead display two emote messages, like this:

Code:
[Mon Sep 08 17:49:52 2014] You say, '#cast 93'
[Mon Sep 08 17:49:52 2014] Your Earring of Influxed Gravity begins to glow.
[Mon Sep 08 17:49:52 2014] The Idol of Rallos Zek singes as the Burst of Flame hits them.
[Mon Sep 08 17:49:52 2014] The Idol of Rallos Zek singes as the Burst of Flame hits them.
[Mon Sep 08 17:49:53 2014] You say, '#cast 93'
[Mon Sep 08 17:49:53 2014] Your Earring of Influxed Gravity begins to glow.
[Mon Sep 08 17:49:53 2014] Your target resisted the Burst of Flame spell.
(I am however using a very old client)

The cause is because spell_effects.cpp floors the damage when it converts to an int32:

dmg = (int32) (dmg * partial / 100);

And low damage spells and/or very low partial rolls will result in a damage that is < 1.

In fact since spells can't hit for anywhere near full damage at near-immunity resist levels, if you for example cast burst of flame on a target with 570 fire resist, every single non-resist will hit for 0 damage.

partial_modifier = ((150 * (resist_chance - roll)) / resist_chance);

Fill in 570 for resist chance (mob resist value, modified by level and such if applicable) and 200 for roll (maximum possible roll) then you get ~97.3684. Floor it since partial_modifier is an int, you get 97. So the absolute highest damage possible spell at 570 resist is 3% the spell's maximum.

ResistSpell() returning 0 will register as a full resist-- it's the non-zero returns that are resulting in 0 damage casts but no resist message. All these casts should not be full resisting either, otherwise you'll end up with NPCs being immune to low damage spells below the 600 threshold. If you view my Soldier of Fire logs at -58 resist debuff, you can see that every single non-resist cast hits for 1 damage (using a 20 damage spell).
Reply With Quote
  #13  
Old 09-26-2014, 05:58 PM
Torven
Sarnak
 
Join Date: Aug 2014
Posts: 52
Default

So as I mentioned before, the purpose of my plotting resist curves was to find a way of estimating mob resist values with a high degree of accuracy. I'm pleased to say I've had much success in that endeavor.

I have written a lua script to feed logs into, and the results look like this:

Code:
Lord Feshlak resists.txt
Disempower - 1174 casts; margin of error: 2.8%
Full Hits: 277 (23.5%)  Full Resists: 897 (76.4%)
   Est Resist Value Based on Full Resists: 193 +/-6

Burst of Flame - 1108 casts; margin of error: 2.9%
Full Hits: 539 (48.6%)  Hits: 899 (81.1%)  Full Resists: 209 (18.8%)
   Est Resist Value Based on Full Hits: 143 +/-6
   Est Resist Value Based on Full Resists: 154 +/-18

Frost Rift - 1091 casts; margin of error: 2.9%
Full Hits: 460 (42.1%)  Hits: 887 (81.3%)  Full Resists: 204 (18.6%)
   Est Resist Value Based on Full Hits: 156 +/-6
   Est Resist Value Based on Full Resists: 153 +/-18

Tainted Breath - 1079 casts; margin of error: 2.9%
Full Hits: 576 (53.3%)  Full Resists: 503 (46.6%)
   Est Resist Value Based on Full Resists: 134 +/-6

Sicken - 1079 casts; margin of error: 2.9%
Full Hits: 561 (51.9%)  Full Resists: 518 (48%)
   Est Resist Value Based on Full Resists: 137 +/-6
--------------------------------------------------------------------------
Lady Mirenilla Ice Strike.txt
Ice Strike - 417 casts; margin of error: 4.7%
Full Hits: 0 (0%)  Hits: 202 (48.4%)  Full Resists: 215 (51.5%)
   Est Resist Value Based on Full Hits: >240
   Est Resist Value Based on Full Resists: 350 +/-29
   Est Resist Value Based on Lowest Non-Crit Partial Mod: 368 +/-3
   Est Resist Value Based on Lowest Crit Partial Mod: 372
I am now in the process of filling out my spreadsheet with raid boss resist values. Anybody else interested in this kind of data collecting can find my lua script here:

https://drive.google.com/open?id=0B9...MGs&authuser=0

Instructions on use can be found in the file. (bossresists.lua)
Reply With Quote
  #14  
Old 07-23-2015, 06:19 AM
Torven
Sarnak
 
Join Date: Aug 2014
Posts: 52
Default

I made a discovery since my last reply that is significant and I want emus to be aware of it.

Sony implemented a way of overriding the resist adjust for NPC casted spells on a per-NPC basis. This means that when you read the spell data for a spell a NPC casts, it may in fact be less resistable than it appears.

I discovered this while parsing Terror's (the golem in fearplane) resists. His AoE was hitting me even though I had over 600 cold resist and was level 90. The spell data on his AoE shows no resist modifier. (it's the same AoE Vox uses) The only way his AoE could be hitting me would be if that specific NPC had a resist adjust modifier for his AoE that overrides the spell data.

I also noticed this when I was parsing Sontalak and Klandicar in Western Wastes. Their fear was hitting me even though I was level 90 with 600+ resists.

I noticed the emu actually had an unfinished implementation of this modifier, but the editor did not have the input for it. One of the emu coders either discovered this or they just wanted the option available. For TAKP I had Cavedude update the editor to include the input. Terror would be a loot pinata without it. (his modifier is quite large, perhaps -400 or -500)

There is a simple method to estimate what the resist adjust is. If you take a character having the same level as the NPC and arrange his resists to a level where a significant amount of both hits and resists will occur, you can come up with a ratio from which you can estimate a resist adjust from.

For example, if you have 500 resist value and the spell hits you 25% of the time, the resist modifier would be -350 because 2 resist value translates into 1% rate rate. (for all-or-nothing spells or full damage DD hits)

Using a character that is not the same level as the NPC makes the math a bit harder. Resist bonus from level disparity is level^2/2. The problem with this is that I don't know what the caps are (if any) or if the rules change after level 66 like PCvsNPC resists.
Reply With Quote
  #15  
Old 09-01-2015, 08:15 AM
Torven
Sarnak
 
Join Date: Aug 2014
Posts: 52
Default

I've made some recent discoveries that invalidate my last post. I jumped to a conclusion with insufficient evidence.

The reason why those AoEs hit high resist characters is not due to a resist adjust, it's because field 'unknown191' (what allakhazam labels it) is a maximum effective resist value. So Dragon Roar, Lava Breath, and Frost Breath all ignore resists over 150 on Live currently (after factoring in level difference). Sony gave those spells a maximum resist a couple of weeks prior to the first progression server launches according to Lucy history. All the raid bosses that use those spells are affected-- Telkorenar for example was hitting my level 90 monk with Lava breath. I parsed the hit rate of these spells and came up with estimates of ~150 effective resist value even when wearing more than that much.

Incidentally my 90 bard was resisting all Lava Breath and Frost Breath spells (Dragon Roar still hit) at any resist level (even naked) for whatever reason.

The resist system was modified substantially during Luclin, so perhaps this was their way to compensate for the more forgiving newer resist system, or compensate for other additions, or just because the dragons are too easy.
Reply With Quote
Reply

Thread Tools
Display Modes

Posting Rules
You may not post new threads
You may not post replies
You may not post attachments
You may not edit your posts

BB code is On
Smilies are On
[IMG] code is On
HTML code is Off

Forum Jump

   

All times are GMT -4. The time now is 05:34 AM.


 

Everquest is a registered trademark of Daybreak Game Company LLC.
EQEmulator is not associated or affiliated in any way with Daybreak Game Company LLC.
Except where otherwise noted, this site is licensed under a Creative Commons License.
       
Powered by vBulletin®, Copyright ©2000 - 2024, Jelsoft Enterprises Ltd.
Template by Bluepearl Design and vBulletin Templates - Ver3.3