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-04-2021, 02:41 AM
Torven
Sarnak
 
Join Date: Aug 2014
Posts: 52
Default 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%)


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.
Reply With Quote
  #2  
Old 08-04-2021, 02:45 AM
Torven
Sarnak
 
Join Date: Aug 2014
Posts: 52
Default

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
Reply With Quote
  #3  
Old 02-17-2024, 11:01 PM
Kaludar
Fire Beetle
 
Join Date: Feb 2009
Posts: 1
Default

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

Last edited by Kaludar; 02-18-2024 at 02:12 AM.. Reason: mez
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 01:09 PM.


 

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