EQEmulator Forums

EQEmulator Forums (https://www.eqemulator.org/forums/index.php)
-   Development::Development (https://www.eqemulator.org/forums/forumdisplay.php?f=590)
-   -   Charm, Root and Lull Resist Mechanics (https://www.eqemulator.org/forums/showthread.php?t=43370)

Torven 08-04-2021 02:41 AM

Charm, Root and Lull Resist Mechanics
 
Charm works like this: (although minor unknown unknowns may exist)

On spell lands, the target's resist is modified by (CHA - 75) / 8, max 25. So if the enchanter has 275+ CHA then they'll get a -25 MR bonus to casts, making it considerably easier to land. If CHA is under 75, there is no penalty to having lower CHA.

Charm tick saves are handled this way:
  • Tick saves in EQ do more than one roll. The first roll is a chance to roll against the MR. This roll is 50% for charm. If this roll fails, charm will hold without checking MR.
  • The caster gets a +4 level bonus when rolling against MR. This makes charm hold better on targets near the caster's level.
  • Resist adjust modifiers on the spell DO apply to tick save throws. (Boltran's has a -10 resist mod)
  • The target's effective MR resist is floored at 5 points, and that is what causes a break chance on an otherwise 0 MR target.
  • Live's resist roll is 1-200. Al'Kabor's was either 0-200 or 0-199. Both use a >, not a >=. This means the charm break resist check is 2.5% for Live and 3% for AK. (since there is a 50% prior roll, break chance per tick is half these %s before TD)
  • Charisma DOES NOT affect the target's chance of breaking charm at all.
  • Total Domination is merely a 15%/25%/35% chance (for ranks 1-3) to keep holding if the caster would otherwise lose the tick save throw.

Root tick saves work like this:
  • Like charm, root has a preliminary roll. This roll is 75%. If this roll fails, root will hold without checking MR. I.e. a 1 in 4 chance to NOT check MR.
  • The caster gets a +4 level bonus when rolling against MR like charm does.
  • Resist adjust modifiers on the spell DO apply to tick save throws.
  • The MR resist is floored at 5 points, and that is what causes a break chance on an otherwise 0 MR target.
  • Live's resist roll is 1-200. Al'Kabor's was either 0-200 or 0-199. Both use a >, not a >=.

Lull fails work like this:
  • When a lull spell lands on a NPC, the NPC's MR is ignored and instead 15 MR is used.
  • The 15 MR is still modified by level difference, meaning mobs 6 levels and under below the caster will never aggro.
  • If the spell resists, then a second roll is done using the caster's charisma. This roll is: 90 - CHA / 4. Failing this roll results in a critical fail and the NPC aggroing on the caster; otherwise the NPC will ignore the resist.
  • If there is a cap to the charisma critical failure chance, then it's not seemingly hit at 305 CHA. I did not parse beyond 305.
  • Lulls have a chance to fade early. The precise logic is unknown, however parses resulted in this:
    • 7% per tick on a +5 a red con
    • 2% per tick on white cons
    • 1% per tick on a -1 blue.
    • 0% per tick on a -5 blue

Evidence For These Claims

I have two primary sources for these claims. I recently automated casts on NPCs for weeks and I was also given information from two highly respected members of the Al'Kabor community who got information years ago from quote 'somebody who ought to know'. They are essentially intermediaries to a real source, but for simplicity I'll refer to these two individuals as 'my sources' for the rest of this thread. Once I was given the information from them I set out to try and verify it with Live server tests, which wasn't simple because they misinterpreted some things, but I was able to figure out what they mistook. I also have some secondary sources, including public dev comments. (which are remarkably inaccurate, but did help in some ways)

I was told by my sources that charm's save throw preliminary roll is 40%, the minimum 'can't go below 5%', the resist check has a +4 level mod, the CHA bonus is CHA - 75 / 8 (max 25), and TD is 15/25/35%. At first I was excited to see this and plugging that into my simulator produced results close enough that I thought it was accurate. Furthermore, the 5% minimum had been mentioned in a 2015 dev post and TD %s were added to the client AA description some years back, so that matched and gave it credibility. I knew that a preliminary roll had to exist simply because 5% by itself didn't work. It looked really legit.

I decided to run tests on Live and also squeeze more AK data from old logs to verify the claims using log parsing scripts I had written. It didn't take long to realize that it didn't work. It was kind of close but too far away to be right. After weeks of data collection on Live and playing with with simulations to try and match it, I found out that the best fit to my Live data is 47% with a MR floor of 5. This got my sources talking more and they mentioned a 'Rand0()' function to me. They got the preliminary roll wrong because they assumed that 'Rand0(4)' meant roll a number from 0 to 4, so comparing that with a 2 (they mentioned a '< 2' here) would result in a 40% per value instead of 50%. EQ's random functions are known to use 0 to n - 1 as client decompiles show. (thanks to Mackal for the assist here) Root data also proved the n - 1 to be correct, so charm's preliminary roll is 50%.

The '5% minimum' could also mean one of several different things. The two most obvious would be either a MR floor of 10 (since the resist roll is 1-200) or a separate 1 in 20 roll. Neither of these worked in simulations. Trial and error to get the simulations to fit the data resulted in an MR floor of 5, but that's not 5%. Further correspondence with my sources later mentioned quote: "I got the strong impression that the person we talked to at least thought that the 5% minimum was not a separate roll but tacked onto the end of the resist function itself." So that strongly implies a MR floor instead of a separate roll. I tested this on a 5 MR NPC and the result was an average duration within the margin of error of a 0 MR NPC, which confirmed it since a separate roll would result in a much lower average.

There is a 2015 dev post here: https://forums.daybreakgames.com/eq/.../#post-3678593
That says: (I added what the SPAs are)
Quote:

Originally Posted by niente
CHA currently affects:

how much merchants will charge you
bard fizzle chance
Chance of an NPC aggro'ing you when you cast pacify
Chance for SPA 63 to successfully blur your target
Chance for DI spells to heal for their full amount
Chance for SPA 22(charm), 31(mez), 34(confuse), 63(blur) to be resisted, you hit this cap at 200 CHA
Note SPA 3(movement rate?), 20(blind), 22(charm), 99(root) have a minimum 5% chance of breaking every tick regardless of how much CHA you have. (only 22 gets a CHA bonus)

Edit: If you have 342 or greater CHA you have capped your bonus chance to all of the above.

Prathun's pseudocode says 'resist chance' in place of the variable that is in actually the target's effective resist VALUE. Since the roll is 200 and not 100, it's essentially the resist chance multiplied by 2. Sony's resist code likely names their effective resist value variable 'resist_chance' or similar. In fact my sources told me: "We can confirm that the general resist check contains something along the lines of "if resist_chance < 5, then resist_chance = 5"" Niente and my sources' contact seem to both have failed to take that into account, resulting in erroneous statements. Even had they said 2.5%, (AK is 3%) they'd still be misleading because charm breaks are two rolls (3 with TD) which need careful explanation and the preliminary roll was omitted entirely in Niente's post. Live's minimum break chance per tick is actually 1.25%. (without TD) There are 10 ticks a minute so at 5% it clearly doesn't work.

Prathun's pseudocode link for reference: https://everquest.allakhazam.com/for...01109310546959

Niente also states that charm tick saves get a CHA bonus. This is incorrect. I ran tests at various CHA levels and they resulted in the same duration charms with large data sets ruling out margin of error. (well over ten thousand casts) In fact due to margin of error, the 15 CHA log resulted in the longest charms in my unicorn data. Niente also implies due to her wording that the CHA bonus to charm caps at 200, which is also untrue for charm lands; although perhaps she just meant a 200 cap for blurs or meant CHA above 75. These are good examples why developer comments should not be taken as gospel and that hard data should be the final arbiter.

Geoffrey Zatkin answered a question in 1999 where he claimed that charisma had a role in charm durations. He was also either wrong or charm changed before PoP went Live. He also mentioned a softcap at 200 CHA in the year 2000 which is not observed in either charm or lull, but I couldn't say how CHA works for mez and mem blur. As a developer myself I can attest that it's easy to get stuff wrong when taking a cursory look at code, so disputing developer claims isn't as presumptuous as it seems.

https://www.tapatalk.com/groups/othe...sma-t1046.html
https://web.archive.org/web/20030611...60&Action=View


Charm Data

Now on to the raw data. I'll start with a link to 2003 data from the enchanter forums on Wayback: https://web.archive.org/web/20050224...pic.php?t=1148

I was very fortunate to find that link while in the middle of my data collecting, because not only is it in and of itself convincing evidence for CHA doing nothing to modify charm durations, but also dates this to 2003, which is the era I'm most interested in; and since this agrees with my Live data, it suggests that charm has not been modified much since then. This guy ran a careful test and lists individual charm durations with known de/buffs and charisma, so this is good stuff. (note that the character's name is Yandie in the table below)

My data comes from two primary sources: Al'Kabor logs, and Live tests I conducted using automated casts. I wrote a script to parse logs to extract charm data, which I will include below. I will present the data in table form because there is a lot of it. The script output looks like this however:

Code:

July 2021: Level 65 Enchanter with 280 CHA and TD3 casting Command of Druzzil vs a gantru moktor (lvl56; 35 MR)
durations: 76, 65, 76, 58, 7, 76, 76, 47, 76, 76, 76, 58, 47, 76, 76, 77, 39, 76, 77, 77, 31, 28, 77, 63, 56, 50, 76, 76, 77, 76, 69,
76, 77, 77, 50, 76, 76, 76, 38, 53, 75, 76, 35, 76, 76, 76, 77, 77, 76, 4, 74, 51, 44, 77, 46, 77, 4, 76, 77, 55, 26, 76, 61, 76, 76,
77, 76, 76, 76, 77, 76, 18, 77, 76, 76, 6, 67, 31, 76, 34, 41, 26, 27, 76, 76, 34, 76, 76, 57, 76, 2, 76, 3, 76, 76, 48, 77, 77, 77,
68, 77, 42, 76, 67, 77, 77, 45, 5, 77, 22, 48, 76, 40, 76, 76, 76, 68, 26, 77, 77, 77, 77, 68, 76, 77, 39, 41, 5, 4, 76, 2, 32, 77,
77, 19, 76, 52, 77, 5, 58, 75, 77, 77, 77, 10, 76, 0, 51, 77, 76, 77, 66, 16, 76, 12, 76, 76, 6, 4, 37, 50, 10, 57, 77, 77, 77, 72,
6, 16, 76, 19, 76, 76, 57, 77, 76, 76, 59, 43, 76, 76, 3, 52, 77, 7, 12, 40, 34, 32, 76, 5, 60, 32, 62, 13, 77, 47, 15, 77, 77, 77,
76, 76, 76, 67, 45, 12, 77, 11, 29, 76, 76, 77, 76, 76, 43, 76, 76, 10, 66, 76, 77, 60, 6, 76, 76, 66, 19, 37, 66, 37, 77, 76, 76,
41, 76, 76, 76, 76, 76,
Command of Druzzil casts: 240;  Lands: 240 (100.00%);  Resists: 0 (0.00%);  Breaks: 240
min duration: 0 ticks;  max duration: 77 ticks;  avg duration: 58.5 ticks;  est max charms: 129 (53.75%)


July 2021: Level 65 Enchanter with 15 CHA, no TD vs 'a unicorn' (lvl 37; est MR 155) casting Beguile
durations: 2, 6, 1, 6, 1, 1, 13, 2, 0, 0, 4, 0, 3, 4, 3, 2, 1, 5, 6, 11, 1, (snip)
Beguile casts: 7012;  Lands: 2985 (42.57%);  Resists: 4027 (57.43%);  Breaks: 2985
min duration: 0 ticks;  max duration: 26 ticks;  avg duration: 3.1 ticks;  est max charms: 3 (0.10%)
0 tick charms: 767 (25.70%)
1 tick charms: 604 (20.23%)
2 tick charms: 457 (15.31%)
3 tick charms: 330 (11.06%)
4 tick charms: 248 (8.31%)
5 tick charms: 158 (5.29%)

https://i.imgur.com/d0oRjiy.png

Note the following:
  • I am able to determine an NPC's resists by parsing spell casts on it. Generally I use a wizard's temperate flux staff for MR. For all-or-nothing spells, the effective resist value is merely resist rate * 2. E.g. 50% resist rate = 100 MR.
  • The resist modifier from level difference is: diff^2 / 2, capped at -40. Charm ticks add +4 to the caster's level here. For example, the Crystalline golem's effective MR vs a level 65 would be: 50 - INT((65-62)^2 / 2) = 46. But for charm ticks: 50 - INT((69-62)^2 / 2) = 26.
  • The '1-200' and '0-200' columns are the results of simulations (the average of 150,000 charms) using those rolls and with a MR floor of 5. ERV = effective resist value.
  • Check the golem and unicorn data-- which were done at various CHA levels-- to see that CHA is modifying the target's MR on lands and not ticks. There are many thousands of casts proving this. I wore a significant amount of -CHA gear to get down to as low as 15.
  • The 5 MR parse of the Crystalline Arachnae more or less proves that they are using a MR floor and not a separate roll.
  • The AK data is clearly lower than the Live data. Since a 5 MR floor is very sensitive to how the 200 roll is done, I endeavored to try and determine precisely how Sony does this, which I outline here: http://www.eqemulator.org/forums/sho...6&postcount=19 The evidence is strongly in favor of AK having had a 0-200 roll and Live using a 1-200 roll and would explain why AK charms were somewhat shorter. Rashere's 2006 resist tinkering could have been the point it changed.
  • Boltran's resulted in longer charms on a 50 MR green NPC, proving that the spell's resist adjust works on tick saves. Level advantage caps at -40, so the spell's resist adjust is lowering it down to the 5 MR floor.
  • The simulations don't fit the unicorn data very well. It's possible that some minor factor is not being accounted for that only shows up when parsing very short duration charms, which may be from how the data is collected or simulated or Sony is doing something I'm unaware of.
  • The PoValor and HoH NPCs had higher MR in 2003 and on AK. I estimate it was 65. Today it's 50. At some point Sony reduced resists on almost all PoP NPCs (perhaps in 2006 by Rashere)
  • Some of the AK data is better than others, as it was from logs of characters leveling up and not clean automated tests like the Live data. The Herv and Torvie data is pretty solid however, as is the 150 cast druid sample, which was on a green mob and farming in a low level zone. The other druid logs should be considered less reliable.
The logs I created from automated casting on Live servers can be downloaded here: https://drive.google.com/drive/folde...Gg&usp=sharing


Root Data

Root and charm work in a similar manner. My sources claimed at first that root's preliminary roll was 60%, but once the Rand0() mistake was corrected, the roll is now known to be 75%. i.e. Rand0(4) < 3. Simulations using a preliminary roll of 75% and a resist floor of 5 matched my Live data very well. Since this fit so well, this bolsters the claims made for charm.

Here is my Live data with simulations under each data set for comparison. Note that 'Test' NPCs on the Test server have a base MR of 50 and that root also gets a +4 level modifier.

Code:

Three Minute Roots
==================

Effective MR 115
----------------
[2021] Torria 65 Enchanter vs a unicorn (lvl36; MR 155) using Paralyzing Earth (0)
Landed roots: 593;  avg duration: 1.7 ticks (5.5%);  Resists: 823 (58.12%)

Simulating 100000 casts at 155 resist value; caster level 65 target level 36; MinResist == 5;  Use floor: true
Initial Effective resist value: 115;  tick effective value: 115;  Break Check Chance: 75
full resists: 57358 (57.358%); lands: 42642 (42.642%); avg duration ticks: 1.78 (5%); max duration roots: 0 (0%)


Effective MR 42
---------------
[2018] Level 65 caster vs. Test Sixty Five (MR 50) using Paralyzing Earth (0)
Roots: 457;  max duration: 31 ticks;  avg duration: 6.0 ticks (19.35%);  max roots: 4 (0.88%);  Resists: 172 (27.34%)

Simulating 100000 casts at 50 resist value; caster level 65 target level 65; MinResist == 5;  Use floor: true
Initial Effective resist value: 50;  tick effective value: 42;  Break Check Chance: 75
full resists: 25123 (25.123%); lands: 74877 (74.877%); avg duration ticks: 5.72 (18%); max duration roots: 425 (0%)


Effective MR 38 (land) 10 (tick)
--------------------------------
[2021] Fenaminae 65 Enchanter vs. Test Sixty (MR 50) using Paralyzing Earth (0)
Roots: 524;  max duration: 31 ticks;  avg duration: 17.2 ticks (55.48%);  max roots: 158 (30.15%);  Resists: 133 (20.24%)

Simulating 100000 casts at 50 resist value; caster level 65 target level 60; MinResist == 5;  Use floor: true
Initial Effective resist value: 38;  tick effective value: 10;  Break Check Chance: 75
full resists: 19150 (19.15%); lands: 80850 (80.85%); avg duration ticks: 17.87 (57%); max duration roots: 25197 (31%)


Effective MR 10
---------------
[2018] Level 65 caster vs. Test Fifty Five (MR 50) using Paralyzing Earth (0)
Roots: 223;  max duration: 31 ticks;  avg duration: 18.1 ticks (58.39%);  max roots: 71 (31.84%);  Resists: 14 (5.91%)

Simulating 100000 casts at 50 resist value; caster level 65 target level 55; MinResist == 5;  Use floor: true
Initial Effective resist value: 10;  tick effective value: 10;  Break Check Chance: 75
full resists: 5141 (5.141%); lands: 94859 (94.859%); avg duration ticks: 17.83 (57%); max duration roots: 29401 (30%)


Effective MR 8 (land) 0 (tick)
------------------------------
[2021] Torria 65 Enchanter vs Test Sixty (MR 50) using Greater Fetter (-30)
Roots: 457;  max duration: 32 ticks;  avg duration: 24.0 ticks (75.00%);  max roots: 252 (55.14%);  Resists: 20 (4.19%)

Effective MR 0
--------------
[2018] Level 65 caster vs. a decaying skeleton (MR 25?) using Paralyzing Earth (0)
Roots: 236;  max duration: 31 ticks;  avg duration: 23.6 ticks (76.13%);  max roots: 140 (59.32%)

Simulating 100000 casts at 25 resist value; caster level 65 target level 1; MinResist == 5;  Use floor: true
Initial Effective resist value: -15;  tick effective value: 5;  Break Check Chance: 75
full resists: 0 (0%); lands: 100000 (100%); avg duration ticks: 23.10 (74%); max duration roots: 56203 (56%)



Two Minute Roots
================

Effective MR 0
--------------
[2015] Level 90? caster vs a decaying skeleton (MR 25?) using Instill (0)
note: this log has weird max duration values, with a lot of 18s and 17s but no 16s and only a few 14s, 15s
Roots: 225;  max duration: 20 ticks;  avg duration: 14.8 ticks (74.00%);  max roots: 44 (19.56%) 53%-68%


Effective MR 50
---------------
[2018] Level 65 caster vs Test Sixty Five (MR 50) using Instill (0)
Roots: 299;  max duration: 17 ticks;  avg duration: 5.4 ticks (31.76%);  max roots: 21 (7.02%)

Shout-out to Kayen who almost nailed the root logic years ago from parsing it. He had come up with 70% + a 5 MR floor, so the emus had it pretty close already. His charm logic worked pretty well too.

I did some root early break logs awhile back (breaks from spell damage) but I can't seem to find them. I believe the chance was 50% or around there on white cons with some scaling to level difference.

The root logs which produced the above data can be downloaded here: https://drive.google.com/drive/folde...4Q&usp=sharing


Lull Data

Prathun's resist pseudocode had the 15 MR substitution for lull spells in it, so that part was easy. Note that this lull behavior was the result of a patch in the late Luclin era and is different for most of Luclin and prior eras. They might have just did the 15 MR substitution then and the rest of this applied to prior eras but I have no idea.

My sources gave me the lull formula of 90 - CHA / 4. I had already done lull parses in 2015 for TAKP so I had data on-hand to compare with and it matched. The formula I came up with for TAKP was only slightly different. This matching also bolsters the charm and root claims.

Here are the notes I wrote 6 years ago:

Code:

Level 50 Enchanter with 62 Charisma casting Pacify on Test Fifty Five:
2428 casts; 2111 hits; 317 resists (13.05%)
Aggros on resists: 241 (76% of resists, 9.9% of casts)

Level 50 Enchanter with 115 Charisma casting Pacify on Test Fifty Five:
671 casts; 585 hits; 86 resists (12.8%)
Aggros on resists: 53 (61.3% of resists, 7.9% of casts)

Level 50 Enchanter with 159 Charisma casting Pacify on Test Fifty:
1482 casts; 1365 hits; 117 resists (7.89%)
Aggros on resists: 52 (44.44% of resists, 3.5% of casts)

Level 50 Enchanter with 200 Charisma casting Pacify on Test Fifty Five:
1496 casts; 1294 hits; 202 resists (13.5%)
Aggros on resists: 80 (39.6% of resists, 5.3% of casts)

Level 50 Enchanter with 255 Charisma casting Pacify on Test Fifty:
1865 casts; 1722 hits; 143 resists (7.66%)
Aggros on resists: 36 (25.17% of resists, 1.93% of casts)

Level 65 Enchanter with 305 Charisma casting Pacification on Test Sixty Five:
4833 casts; 4463 hits; 370 resists (7.66%)
Aggros on resists: 51 (13.78% of resists, 1.06% of casts)

Those results suggest that a charisma cap for lulls is not met at 305. Since Al'Kabor's max CHA was 305, they had no reason to cap it then if developers wanted some low minimum fail chance, so my source would not have mentioned a cap but they might have added a cap in later years. I have no idea if lull becomes unaggroable at 360 CHA or not on Live. I'm sure players of later content could answer this.

Lulls can fade early and this is easily seen with 'worn off messages' on Live servers and easily tested for on Test server Arena mobs. My 2015 notes on this are:

"Chance is not affected by MR or charisma.
On Live, fade chance per tick was about 2% per tick on white cons, 7% on a +5 a red con, 0% on a -5 blue, and 1% on a -1 blue."


This data could be fleshed out more, but for TAKP I'm currently using: fadeChance = GetLevel() - caster_level + 2; in DoBuffTic()

My lull logs can be downloaded here: https://drive.google.com/drive/folde...dA&usp=sharing


Additional Claims

My sources claim that blind and fear also use a 75% preliminary roll, and that blind also has that 5 MR floor; however they're unsure if fear uses a 5 MR floor. Awhile back I did raise blind's preliminary roll on TAKP to the same level as root was (70%; but I'll raise them to 75% very shortly) because I did some crude tests on Live and it seemed to need to be raised, so that agrees with the claim. Beyond that I have no data to share.

Torven 08-04-2021 02:45 AM

I use lua scripts to parse logs or run simulations. I will paste them here.

This is my charm simulator:
Code:

local RESIST_VALUE = 50;
local CASTS = 150000;
local CASTER_LEVEL = 65;
local TARGET_LEVEL = 62;
local CHARM_DURATION_TICKS = 76;
--local CHARM_DURATION_TICKS = 180;
local CHARISMA = 280;
local USE_BOW_CURVE = false;        -- PvP resist curve is bow shaped
local USE_FLOOR = true;
local FLOOR_VALUE = 5;
local MIN_ROLL = 1;                                -- use 1 for Live, 0 for AK
local MAX_ROLL = 200;
local CharmBreakCheckChance = 50;
local CharismaEffectiveness = 8;
local TDBonusPct = 0;
--local TDBonusPct = 35;                                                -- TD = 15%, 25%, 35% says raidloot.com

math.randomseed(os.time());

local tickResistChecks, tickResistFails = 0, 0;

function charmResist(charmTick, resistValue, targetLevel, casterLevel, charisma)

        local hitStatus = false;
        if ( charmTick ) then
                casterLevel = casterLevel + 4;
        end
        local levelDiff = targetLevel - casterLevel;
        local tempLevelDiff = levelDiff;
       
        if ( targetLevel >= 67 ) then
                tempLevelDiff = 66 - casterLevel;
                if ( tempLevelDiff < 0 ) then
                        tempLevelDiff = 0;
                end
        end
       
        if ( tempLevelDiff < -9 ) then
                tempLevelDiff = -9;
        end
       
        local levelMod = math.floor(tempLevelDiff * tempLevelDiff / 2);
       
        if ( tempLevelDiff < 0 ) then
                levelMod = -levelMod;
        end
       
        local effectiveResistValue = resistValue + levelMod;
        local bonus = 0;

        if ( not charmTick ) then
       
                if ( CHARISMA >= 75 ) then

                        bonus = math.floor((CHARISMA - 75) / CharismaEffectiveness);
                        if ( bonus > 25 ) then
                                bonus = 25;
                        end
                        effectiveResistValue = effectiveResistValue - bonus;
                end
               
        else
                tickResistChecks = tickResistChecks + 1;
        end


        if ( not USE_FLOOR and charmTick and math.random(20) == 1 ) then -- 5% chance to fail
                hitStatus = false;
        else
                local erv = effectiveResistValue;
                if ( USE_BOW_CURVE and effectiveResistValue < 200 ) then
                        erv = -0.07868992 + 1.53452*effectiveResistValue - 0.002708188*(effectiveResistValue*effectiveResistValue);
                end
               
                if ( charmTick and USE_FLOOR) then
                       
                        if ( erv < FLOOR_VALUE ) then
                                erv = FLOOR_VALUE;
                        end                       
                end
               
                if ( math.random(MIN_ROLL, MAX_ROLL) > erv ) then
                        hitStatus = true;
                       
                else
                        hitStatus = false;
                end
        end
        if ( charmTick and not hitStatus ) then
                tickResistFails = tickResistFails + 1;
        end
        return hitStatus, effectiveResistValue, levelMod;
end

local hitStatus, effectiveResistValue, levelMod, tickHitStatus, tickEffectiveResistValue = 0, 0, 0, 0, 0;
local fullResists, avgDuration, duration, lands, longestCharm, maxCharms = 0, 0, 0, 0, 0, 0;
local durationsStr = "";
local ticks, tickFails1, tdChecks, tdSuccesses = 0, 0, 0, 0;
local tickTable = { [0] = 0, [1] = 0, [2] = 0, [3] = 0, [4] = 0, [5] = 0, };


fullResists, lands, avgDuration = 0, 0, 0;

for i = 1, CASTS do

        hitStatus, effectiveResistValue, levelMod = charmResist(false, RESIST_VALUE, TARGET_LEVEL, CASTER_LEVEL, CHARISMA);
       
        if ( hitStatus ) then
       
                duration = 0;
       
                for j = 1, CHARM_DURATION_TICKS do
               
                        ticks = ticks + 1;
                       
                        -- Mob::PassCharismaCheck() called by spell_effects.cpp does this before calling ResistSpell()
                        if ( math.random(1, 100) > CharmBreakCheckChance ) then
                                tickHitStatus = true; -- charm holds
                        else
                                tickHitStatus = false;
                                tickFails1 = tickFails1 + 1;
                        end

                        if ( not tickHitStatus ) then
                                tickHitStatus, tickEffectiveResistValue = charmResist(true, RESIST_VALUE, TARGET_LEVEL, CASTER_LEVEL, CHARISMA);
                        end
                       
                        if ( not tickHitStatus and TDBonusPct > 0 ) then
                                tdChecks = tdChecks + 1;
                                if ( math.random(1, 100) <= TDBonusPct ) then
                                        tickHitStatus = true;
                                        tdSuccesses = tdSuccesses + 1;
                                end
                        end

                        if ( tickHitStatus ) then
                                duration = duration + 1;
                        else
                                if ( duration < 6 ) then
                                        tickTable[duration] = tickTable[duration] + 1;
                                end
                                break;
                        end
                       
                        if ( j == CHARM_DURATION_TICKS ) then
                                maxCharms = maxCharms + 1;
                        end
                end
               
                if ( duration > longestCharm ) then
                        longestCharm = duration;
                end
                avgDuration = avgDuration + duration;
                lands = lands + 1;
               
                --print(duration);
                --durationsStr = durationsStr..duration..", "
        else
                fullResists = fullResists + 1;
        end
end

avgDuration = avgDuration / lands;

print("Simulating "..CASTS.." casts at "..RESIST_VALUE.." resist value; caster level "..CASTER_LEVEL.."; CHA: "..CHARISMA..
";  target level "..TARGET_LEVEL..";  Roll: "..MIN_ROLL.."-"..MAX_ROLL..";  CharmBreakCheckChance = "..CharmBreakCheckChance..
";  Max Charm Duration: "..CHARM_DURATION_TICKS..";  TD%: "..TDBonusPct);
print("Initial Effective resist value: "..effectiveResistValue..";  tick effective value: "..tickEffectiveResistValue..
";  Floor: "..FLOOR_VALUE.."  UseFloor?  Bow curve?:", USE_FLOOR, USE_BOW_CURVE);
print(string.format("full resists: %i (%0.2f%%); lands: %i (%0.2f%%); avg duration ticks: %0.1f (%0.2f%%); max duration charms: %i (%0.2f%%)",
fullResists, (fullResists/CASTS*100), lands, (lands/CASTS*100), avgDuration, avgDuration/CHARM_DURATION_TICKS*100, maxCharms, maxCharms/CASTS*100));
--print(durationsStr);
print("longest charm: "..longestCharm.." ticks");
print(string.format("base tick fail%%: %0.3f%%  TD success%%: %0.3f%%  tick resist fail%%: %0.3f%%",
(tickFails1/ticks*100), (tdSuccesses/tdChecks*100), (tickResistFails/tickResistChecks*100)));
for i = 0, 5 do print(string.format("%i tick charms: %i (%0.2f%%)", i, tickTable[i], tickTable[i]/lands*100)); end


This is my charm log parser:

Code:

local INPUT_DIR = "I:\\Google Drive\\Classic EverQuest Preservation\\EQLive 2014 Sourced Data\\Logs\\Resist Mechanics Logs\\Charms\\";
--local INPUT_DIR = "I:\\Parse\\";

local INPUT_FILENAME = "eqlog_Torria_test - lvl65 ench CHA15 TD0 vs crystalline golem CoD.txt";
local AK_LOG = false;
local SPELL_NAME = "Command of Druzzil";
local IS_DRUID_SPELL = false;
local SPELL_DURATION = 75; -- duration in ticks  (PoP charms = 75 at 65)
--local TASH_SPELL = "Tashina"; -- nil this if not using a tash spell


function parseTime(line)
        local offset = line:find("%[") - 1;
        return tonumber(line:sub(offset + 10, offset + 11)) * 86400 + tonumber(line:sub(offset + 13, offset + 14)) * 3600 +
tonumber(line:sub(offset + 16, offset + 17)) * 60 + tonumber(line:sub(offset + 19, offset + 20));
end

local charmLandTime, charmEndTime, charmDuration, charmMin, charmSec, charmTicks, castTime, prevLandTime, prevEndTime, lastEndTime = 0, 0, 0, 0, 0, 0, 0, 0, 0, 0;
local charms, avgTicks, maxDuration, minDuration, maxCharms, resists, lands, breaks, ignored, interrupts, invisBreaks = 0, 0, 0, 9999, 0, 0, 0, 0, 0, 0, 0;
local charmActive, tashActive = false, false;
local castingSpell;
local charmList = {};
local s, ts;

local landText = " has been charmed";
local breakText = "Your "..SPELL_NAME.." spell has worn off of ";
local resistText = " resisted your "..SPELL_NAME.."!";
local tashOffText = "Your "..(TASH_SPELL or "nil").." spell has worn off of ";
if ( AK_LOG ) then
        if ( IS_DRUID_SPELL ) then
                landText = " blinks.";
        else
                landText = "You begin casting "..SPELL_NAME;
        end
        breakText = "Your charm spell has worn off";
        resistText = "Your target resisted the "..SPELL_NAME.." spell.";
end

for line in io.lines(INPUT_DIR..INPUT_FILENAME) do

        if ( AK_LOG and line:find("You begin casting ", 28, true) ) then
       
                _, _, castingSpell = line:find("^You begin casting (.+)%.", 28);
                castTime = parseTime(line);
               
                if ( not IS_DRUID_SPELL and castingSpell == SPELL_NAME ) then
                        prevLandTime = charmLandTime;
                        prevEndTime = charmEndTime;
                       
                        charmLandTime = castTime;
                        charmEndTime = 0;
                        lands = lands + 1;
                        if ( AK_LOG and charmActive ) then
                                lands = lands - 1;
                        end
                        charmActive = true;
                end
       
        elseif ( (not AK_LOG or IS_DRUID_SPELL) and line:find(landText, 28, true) and (not TASH_SPELL or tashActive) ) then
                charmLandTime = parseTime(line);
                charmEndTime = 0;
                lands = lands + 1;
                charmActive = true;
       
        elseif ( charmActive and line:find(breakText, 28, true) ) then
                charmEndTime = parseTime(line);
                lastEndTime = charmEndTime;
                breaks = breaks + 1;
                charmActive = false;
               
                if ( charmLandTime > 0 and charmEndTime > 0 ) then
                       
                        charmDuration = charmEndTime - charmLandTime;
                       
                        if ( charmDuration > ((SPELL_DURATION+3) * 6) or charmDuration < 0 ) then
                                ignored = ignored + 1;
                        elseif ( TASH_SPELL and not tashActive ) then
                                ignored = ignored + 1;                       
                        else
                                charms = charms + 1;
                                avgTicks = avgTicks + charmDuration;
                                charmLandTime = 0;
                                charmEndTime = 0;
                                charmMin = math.floor(charmDuration / 60);
                                charmSec = charmDuration % 60;
                                if ( charmSec < 10 ) then
                                        charmSec = "0"..charmSec;
                                end
                                charmTicks = math.floor(charmDuration / 6);
                                if ( charmTicks > maxDuration ) then
                                        maxDuration = charmTicks;
                                end
                                if ( minDuration > charmTicks ) then
                                        minDuration = charmTicks;
                                end
                                table.insert(charmList, charmTicks);
                        end               
                end       
               
                if ( charmDuration > 0 ) then
                        --print(line.."\t"..charmMin..":"..charmSec.."\t"..charmTicks.." ticks\n");
                        charmDuration = 0;
                end
               
        elseif ( AK_LOG and not IS_DRUID_SPELL and castingSpell == SPELL_NAME and line:find("Your spell is interrupted", 28, true) ) then
                if ( parseTime(line) - castTime < 5 ) then
               
                        charmLandTime = prevLandTime;
                        charmEndTime = charmEndTime;
                        interrupts = interrupts + 1;

                        --print("Possible charm cast interrupt:");
                        --print(line);
                end
               
        elseif ( line:find(resistText, 28, true) ) then
                resists = resists + 1;
               
        elseif ( TASH_SPELL and line:find(" glances nervously about.", 28, true) ) then
                tashActive = true;
               
        elseif ( TASH_SPELL and line:find(tashOffText, 28, true) ) then
                tashActive = false;
               
        elseif ( (line:find("You vanish.", 28, true) or line:find("Your body fades away.", 28, true) or line:find("You are no longer hidden.", 28, true))
                and lastEndTime == parseTime(line)
        ) then
                invisBreaks = invisBreaks + 1;
                charmList[#charmList] = -charmList[#charmList];
        end

end
       
if ( charms > 0 ) then
        avgTicks = avgTicks / charms / 6;
       
        s = "durations: ";
       
        for i, tick in ipairs(charmList) do
                if ( tick < 0 ) then
                        ts = "("..tostring(-tick)..")";
                else
                        ts = tostring(tick)
                end
                s = s..ts..", ";
                if ( tick == maxDuration or tick == (maxDuration - 1) ) then
                        maxCharms = maxCharms + 1;
                end
        end
        print(INPUT_FILENAME);
        print(s);
       
        print(string.format(SPELL_NAME.." casts: %i;  Lands: %i (%0.2f%%);  Resists: %i (%0.2f%%);  Breaks: %i",
        resists+lands, lands, lands/(lands+resists)*100, resists, resists/(lands+resists)*100, breaks));
       
        if ( ignored > 0 or interrupts > 0 or invisBreaks > 0 ) then
                print(string.format("Ignored: %i;  Interrupts: %i;  Invis Breaks: %i", ignored, interrupts, invisBreaks));
        end
       
        print(string.format("min duration: %i ticks;  max duration: %i ticks;  avg duration: %0.1f ticks;  est max charms: %i (%0.2f%%)",
        minDuration, maxDuration, avgTicks, maxCharms, maxCharms/(breaks-ignored)*100));
       
        local x;
        for j = 0, 5 do
                x = 0;
                for i, tick in ipairs(charmList) do
                        if ( tick == j ) then
                                x = x + 1;
                        end
                end
                print(string.format("%i tick charms: %i (%0.2f%%)", j, x, x/lands*100));
        end
else
        print("no charms found");
end


This is my root simulator:

Code:

local RESIST_VALUE = 50;
local CASTS = 100000;
local CASTER_LEVEL = 65;
local TARGET_LEVEL = 65;
local ROOT_DURATION_TICKS = 30;
local USE_FLOOR = true;

local RootBreakCheckChance = 75;
local RootMinResist = 5;

math.randomseed(os.time());

function RootResist(rootTick, resistValue, targetLevel, casterLevel)

        local hitStatus = false;
       
        if ( rootTick ) then
                casterLevel = casterLevel + 4;
        end
       
        local levelDiff = targetLevel - casterLevel;
        local tempLevelDiff = levelDiff;
       
        if ( targetLevel >= 67 ) then
                tempLevelDiff = 66 - casterLevel;
                if ( tempLevelDiff < 0 ) then
                        tempLevelDiff = 0;
                end
        end
       
        if ( tempLevelDiff < -9 ) then
                tempLevelDiff = -9;
        end
       
        local levelMod = math.floor(tempLevelDiff * tempLevelDiff / 2);
       
        if ( tempLevelDiff < 0 ) then
                levelMod = -levelMod;
        end
       
        local effectiveResistValue = resistValue + levelMod;

        if ( rootTick ) then
               
                if ( USE_FLOOR ) then
                        if ( effectiveResistValue < RootMinResist ) then
                                effectiveResistValue = RootMinResist;
                        end
                else
                        if ( math.random(20) == 1 ) then
                                return false, effectiveResistValue, levelMod;
                        end
                end
        end

        if ( math.random(1, 200) > effectiveResistValue ) then
                hitStatus = true;
        else
                hitStatus = false;
        end
        return hitStatus, effectiveResistValue, levelMod;
end

local hitStatus, effectiveResistValue, levelMod, tickHitStatus, tickEffectiveResistValue = 0, 0, 0, 0, 0;
local fullResists, avgDuration, duration, lands, longestRoot, maxRoots = 0, 0, 0, 0, 0, 0;
local durationsStr = "";


fullResists, lands, avgDuration = 0, 0, 0;

for i = 1, CASTS do

        hitStatus, effectiveResistValue, levelMod = RootResist(false, RESIST_VALUE, TARGET_LEVEL, CASTER_LEVEL);
       
        if ( hitStatus ) then

                if ( math.random(100) > 50 ) then
                        duration = 1;
                else
                        duration = 0;
                end
       
                for j = 1, ROOT_DURATION_TICKS do
               
                        tickHitStatus, tickEffectiveResistValue = RootResist(true, RESIST_VALUE, TARGET_LEVEL, CASTER_LEVEL);
                       
                        if ( math.random(0, 99) > RootBreakCheckChance ) then
                                tickHitStatus = true;
                        end
                       
                        if ( tickHitStatus ) then
                                duration = duration + 1;
                        else
                                break;
                        end
                       
                        if ( j == ROOT_DURATION_TICKS ) then
                                maxRoots = maxRoots + 1;
                        end
                end
               
                if ( duration > longestRoot ) then
                        longestRoot = duration;
                end
                avgDuration = avgDuration + duration;
                lands = lands + 1;
               
                --print(duration);
                --durationsStr = durationsStr..duration..", "
        else
                fullResists = fullResists + 1;
        end
end

avgDuration = string.format("%.2f", avgDuration / lands);

print("Simulating "..CASTS.." casts at "..RESIST_VALUE.." resist value; caster level "..CASTER_LEVEL.." target level "..TARGET_LEVEL..
"; MinResist == "..RootMinResist..";  Use floor:", USE_FLOOR);
print("Initial Effective resist value: "..effectiveResistValue..";  tick effective value: "..tickEffectiveResistValue..
";  Break Check Chance: "..RootBreakCheckChance);
print("full resists: "..fullResists.." ("..(fullResists/CASTS*100).."%); lands: "..lands.." ("..(lands/CASTS*100)..
"%); avg duration ticks: "..avgDuration.." ("..math.floor(avgDuration/(ROOT_DURATION_TICKS+1)*100)..
"%); max duration roots: "..maxRoots.." ("..math.floor(maxRoots/lands*100).."%)");
--        print(durationsStr);
print("longest root: "..longestRoot.." ticks");


This is my root log parser:

Code:

local INPUT_DIR = "I:\\Google Drive\\Classic EverQuest Preservation\\EQLive 2014 Sourced Data\\Logs\\Resist Mechanics Logs\\Roots\\";

local INPUT_FILENAME = "eqlog_Torria_test - lvl65 ench Paralyzing Earth vs a unicorn.txt";

function parseTime(line)
        local offset = line:find("%[") - 1;
        return tonumber(line:sub(offset + 10, offset + 11)) * 86400 + tonumber(line:sub(offset + 13, offset + 14)) * 3600 +
tonumber(line:sub(offset + 16, offset + 17)) * 60 + tonumber(line:sub(offset + 19, offset + 20));
end


local rootLandTime, rootEndTime, rootDuration, rootMin, rootsec, rootTicks, maxRoots, resists, clips = 0, 0, 0, 0, 0, 0, 0, 0, 0;
local roots, avgTime, avgTicks, maxDurationTicks, maxDurationSecs, minDurationTicks, minDurationSecs = 0, 0, 0, 0, 0, 0, 9999;
local rootTicksTbl = {};
local rootDurations = {};
local s, now;

for line in io.lines(INPUT_DIR..INPUT_FILENAME) do

        if ( line:find(" adheres to the ground.", 28, true) ) then
                now = parseTime(line);
                if ( rootLandTime > 0 and (now - rootLandTime) < 200 ) then
                        clips = clips + 1;
                end
                rootLandTime = now;
                rootEndTime = 0;
               
        elseif ( line:find("spell has worn off of ", 28, true) ) then
                rootEndTime = parseTime(line);
               
        elseif ( line:find("Your target resisted the ", 28, true) or line:find(" resisted your ", 28, true) ) then
                resists = resists + 1;
        end
       
        if ( rootLandTime > 0 and rootEndTime > 0 ) then
                rootDuration = rootEndTime - rootLandTime;
                roots = roots + 1;
                avgTime = avgTime + rootDuration;
                rootLandTime = 0;
                rootEndTime = 0;
                rootMin = math.floor(rootDuration / 60);
                rootsec = rootDuration % 60;
                if ( rootsec < 10 ) then
                        rootsec = "0"..rootsec;
                end
                rootTicks = math.floor(rootDuration / 6);
                if ( rootTicks > maxDurationTicks ) then
                        maxDurationTicks = rootTicks;
                end
                if ( minDurationTicks > rootTicks ) then
                        minDurationTicks = rootTicks;
                end
                if ( rootDuration > maxDurationSecs ) then
                        maxDurationSecs = rootDuration;
                end
                if ( minDurationSecs > rootDuration ) then
                        minDurationSecs = rootDuration;
                end
                table.insert(rootTicksTbl, rootTicks);
                table.insert(rootDurations, rootDuration);
        end
end
       
if ( roots > 0 ) then
        print(INPUT_FILENAME);
        avgTicks = avgTime / roots / 6;
        avgTicks = math.floor(avgTicks * 10) / 10;
        s = "Tick durations: ";
        for i, tick in ipairs(rootTicksTbl) do
                s = s..tick..", ";
               
                if ( tick == maxDurationTicks or tick == (maxDurationTicks - 1) ) then
                        maxRoots = maxRoots + 1;
                end
        end
        --print(s);
        print(string.format("Landed roots: %i;  max duration: %i ticks;  avg duration: %0.1f ticks (%0.2f%%);  max roots: %i (%0.2f%%)",
        roots, maxDurationTicks, avgTicks, avgTicks/maxDurationTicks*100, maxRoots, maxRoots/roots*100));
        if ( resists > 0 ) then
                print(string.format("Resists: %i (%0.2f%%)", resists, resists/(resists+roots)*100));
        end
        if ( clips > 0 ) then
                print("Clips: "..clips);
        end
       
        --[[
        avgTime = avgTime / roots;
        avgTime = math.floor(avgTime * 10) / 10;
        s = "Second durations: ";
        for i, tick in ipairs(rootDurations) do
                s = s..tick..", ";
        end
        print(s);
        print(string.format("min duration: %is;  max duration: %i s;  avg duration: %i s (%0.2f%%)",
                minDurationSecs, maxDurationSecs, avgTime, avgTime/maxDurationSecs*100));
        ]]
end


Kaludar 02-17-2024 11:01 PM

Is it possible to get a Memory Blur/Mez addendum to this?


All times are GMT -4. The time now is 07:23 PM.

Powered by vBulletin®, Copyright ©2000 - 2024, Jelsoft Enterprises Ltd.